From 0cef414e9af5b4545bff2b6052fe854f757da185 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:19:24 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20Summarum=20=E2=80=94=20natural=20langua?= =?UTF-8?q?ge=20calculator=20notebook=20for=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Write plain text, get live results per line. English + Russian, units, live currency rates with crypto, percents, dates and timezones, bitwise ops, variables and sheet totals. Tauri 2 + TypeScript engine + CodeMirror 6; daily backups and a deleted-sheets bin. Inspired by Numi for macOS (numi.app); built from scratch for Windows. --- .gitignore | 5 + ARCHITECTURE.md | 119 + LICENSE | 6 + README.md | 136 + index.html | 98 + package-lock.json | 2064 ++++++++++ package.json | 37 + scripts/gen-icon.ts | 156 + src-tauri/Cargo.lock | 5745 +++++++++++++++++++++++++++ src-tauri/Cargo.toml | 26 + src-tauri/build.rs | 3 + src-tauri/capabilities/default.json | 25 + src-tauri/extension-sample.js | 21 + src-tauri/icons/128x128.png | Bin 0 -> 971 bytes src-tauri/icons/128x128@2x.png | Bin 0 -> 2015 bytes src-tauri/icons/32x32.png | Bin 0 -> 249 bytes src-tauri/icons/icon.ico | Bin 0 -> 4273 bytes src-tauri/icons/icon.png | Bin 0 -> 4597 bytes src-tauri/src/main.rs | 475 +++ src-tauri/tauri.conf.json | 54 + src/engine/datetime.ts | 109 + src/engine/edge.test.ts | 144 + src/engine/engine.test.ts | 241 ++ src/engine/evaluator.ts | 466 +++ src/engine/extraunits.ts | 98 + src/engine/formatter.ts | 116 + src/engine/index.ts | 217 + src/engine/lexer.ts | 79 + src/engine/parser.ts | 389 ++ src/engine/rates-snapshot.json | 33 + src/engine/registry.ts | 394 ++ src/engine/tokenizer.ts | 128 + src/engine/types.ts | 90 + src/engine/unitdata.ts | 135 + src/engine/vocab-data.ts | 1015 +++++ src/engine/vocab.ts | 72 + src/extensions.ts | 29 + src/i18n.ts | 101 + src/main.ts | 576 +++ src/storage.ts | 241 ++ src/ui/app.css | 399 ++ src/ui/editor.ts | 279 ++ tsconfig.json | 20 + vite.config.ts | 13 + 44 files changed, 14354 insertions(+) create mode 100644 .gitignore create mode 100644 ARCHITECTURE.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 index.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/gen-icon.ts create mode 100644 src-tauri/Cargo.lock create mode 100644 src-tauri/Cargo.toml create mode 100644 src-tauri/build.rs create mode 100644 src-tauri/capabilities/default.json create mode 100644 src-tauri/extension-sample.js create mode 100644 src-tauri/icons/128x128.png create mode 100644 src-tauri/icons/128x128@2x.png create mode 100644 src-tauri/icons/32x32.png create mode 100644 src-tauri/icons/icon.ico create mode 100644 src-tauri/icons/icon.png create mode 100644 src-tauri/src/main.rs create mode 100644 src-tauri/tauri.conf.json create mode 100644 src/engine/datetime.ts create mode 100644 src/engine/edge.test.ts create mode 100644 src/engine/engine.test.ts create mode 100644 src/engine/evaluator.ts create mode 100644 src/engine/extraunits.ts create mode 100644 src/engine/formatter.ts create mode 100644 src/engine/index.ts create mode 100644 src/engine/lexer.ts create mode 100644 src/engine/parser.ts create mode 100644 src/engine/rates-snapshot.json create mode 100644 src/engine/registry.ts create mode 100644 src/engine/tokenizer.ts create mode 100644 src/engine/types.ts create mode 100644 src/engine/unitdata.ts create mode 100644 src/engine/vocab-data.ts create mode 100644 src/engine/vocab.ts create mode 100644 src/extensions.ts create mode 100644 src/i18n.ts create mode 100644 src/main.ts create mode 100644 src/storage.ts create mode 100644 src/ui/app.css create mode 100644 src/ui/editor.ts create mode 100644 tsconfig.json create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..29dd4b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +src-tauri/target/ +src-tauri/gen/ +*.log diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..7142d22 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,119 @@ +# Architecture + +For people who want to read or change the code. The app is two parts: +a **TypeScript calculation engine + UI** running in WebView2, and a thin +**Rust shell** (Tauri 2) for everything OS-specific. + +``` +src/ + engine/ the calculator itself (pure TS, no DOM) + lexer.ts line → raw lexemes (numbers, words, symbols) + vocab-data.ts en+ru word tables: unit names, currencies, operators + vocab.ts merged lookup over the tables + unitdata.ts conversion ratios (base units per dimension) + extraunits.ts speed/pressure/energy/power/frequency/fuel + crypto list + registry.ts phrase → token payload; built once at startup + tokenizer.ts lexemes + registry → semantic tokens + parser.ts tokens → AST (noise-tolerant Pratt parser) + evaluator.ts AST + document context → values with units + formatter.ts values → display strings + datetime.ts timezone map, calendar arithmetic + index.ts SumEngine facade + extension API + sheet totals + ui/ + editor.ts CodeMirror 6 wiring: highlight, results overlay, copy + app.css themes (light/dark via CSS variables) + main.ts app bootstrap: documents, settings, status bar, dnd + storage.ts Tauri commands / localStorage fallback + extensions.ts runs user .js files against the engine + i18n.ts interface strings (en/ru) +src-tauri/ + src/main.rs tray, hide-to-tray, storage commands, rates fetching + (open.er-api.com + CoinGecko) with cache, backups, + data-folder migration, file drops +``` + +## How a line becomes a result + +1. **Lexer** splits `"5 inches in cm"` into lexemes: `5`, `inches`, `in`, `cm`. + The same lexer segments vocabulary phrases, so matching is consistent. +2. **Registry** holds every phrase the engine knows, keyed by first lexeme. + Longest match wins; case-sensitive entries (`m` vs `M`, `kB`) are tried + before case-insensitive ones, and an ambiguous lowercase form (`mm` vs `Mm`) + simply doesn't get a case-insensitive fallback. +3. **Tokenizer** walks the lexemes, asks the registry, and emits typed tokens. + A couple of context hacks live here, e.g. `in` right after a number at the + end of a line means inches (`6 ft 3 in`). +4. **Parser** is a Pratt parser that tolerates noise: unknown words are + dropped before parsing (that's why `spent 20 on pizza plus 5` works) — + except words right after a conversion operator, which may name a timezone. + Precedence, loosest to tightest: line-level conversions (`in cm` applies to + the merged line, so `2 hours 30 minutes in minutes` works) → bitwise → + additive → multiplicative (incl. `20% of x`) → unary → postfix + (units, `%`, `!`, `^`) → primary. +5. **Evaluator** computes `{ value: Decimal, unit }` triples. Same-dimension + operands convert through base units (`toBase`/`fromBase`); temperatures + carry an offset, fuel economy units can be reciprocal (mpg ↔ L/100km). + The document context supplies variables and previous line results for + `sum`/`avg`/`prev`. +6. **Formatter** renders the value with the unit's display format, precision + and grouping from settings, or as hex/binary/fraction/roman/etc. + +Numbers are `decimal.js` throughout — no binary float artifacts. + +## Adding things + +**A unit** — add a row to `UNIT_DATA` in `unitdata.ts` (id, dimension, ratio +to the dimension base) and its names to `vocab-data.ts` under the matching +category (`"myunit.variants": "name,names,имя"`, `"myunit.format": "sym"`). +For a brand-new dimension, extend the `Dimension` union in `types.ts` and use +`extraunits.ts` — phrases live inline there. + +**A currency** — fiat codes come from the `Currency` table in `vocab-data.ts`; +crypto goes in `CRYPTO` in `extraunits.ts` (code, CoinGecko id, phrases, +snapshot price) plus the mirror list in `src-tauri/src/main.rs`. + +**An operator/function** — register the phrase in `buildRegistry()` +(`registry.ts`), add a token/AST case if it needs new syntax, implement in +`evaluator.ts`. Most word-operators are data, not code. + +**A timezone city** — one line in the `Z` map in `datetime.ts`. + +## Engine gotchas + +- Phrase conflicts are real: single letters are case-sensitive by design + (`m` meter / `M` million, `k` thousand / `K` kelvin). Prefix generation is + filtered where it collides with words — e.g. seconds only get ms/µs/ns/ps + because an "attosecond" symbol would shadow the word `as`. +- `and`/`or` are dual-purpose: `and` adds (`5 and 3` = 8), `or` is bitwise. + `of/off/on` only act on a percent left-hand side; otherwise they're prose. +- Currency rates are "units per USD"; a currency Unit's ratio is computed at + tokenization time from the current rate map, so re-setting rates re-prices + everything on the next evaluation. +- The registry is rebuilt never; extensions mutate it (add phrases/functions) + at startup. + +## UI notes + +- CodeMirror injects its base theme at runtime *after* `app.css`, so anything + that must win (content padding for the results column, cursor color) lives + in an `EditorView.theme(...)` extension, not the stylesheet. +- The results column is an absolutely-positioned overlay; each result is + placed with `view.lineBlockAt(...)` screen coordinates and re-rendered on + scroll/geometry changes. Click-to-copy reads `data-value`. +- Engine evaluation is synchronous on every keystroke — a full sheet parse + is well under a millisecond, so there's no debounce or worker. + +## Storage + +- `settings.json` always sits in `%APPDATA%/app.summarum.calc`. +- `documents.json` (all sheets) + `backups/` live either there or in the + user-chosen data folder (the path is stored in settings). +- Daily snapshot at startup (`backups/documents-YYYY-MM-DD.json`, keep 14); + deleted sheets become `.numi` files under `backups/deleted/` and are pruned + by age. Saving is debounced at 400 ms. + +## Tests + +`npm test` runs ~80 vitest cases over the engine (`src/engine/*.test.ts`): +every expression class, both languages, deterministic injected rates. +UI is exercised manually; the engine is where the complexity lives. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fea9638 --- /dev/null +++ b/LICENSE @@ -0,0 +1,6 @@ +[Historical MIT license text below has been superseded — see the current LICENSE file for the license actually in effect: PolyForm Noncommercial 1.0.0.] + +Copyright (c) 2026 Joseph Munoz + +[Grant text removed — superseded by PolyForm Noncommercial 1.0.0, see current LICENSE file] +[Remaining historical MIT boilerplate removed — superseded by PolyForm Noncommercial 1.0.0; see current LICENSE file for the license actually in effect.] diff --git a/README.md b/README.md new file mode 100644 index 0000000..73667ff --- /dev/null +++ b/README.md @@ -0,0 +1,136 @@ +# Summarum + +**A calculator that works like a notepad.** Type what you're thinking — every line +gets a live answer in the right column. No buttons, no formula syntax, just text. + +``` +# Trip to Lisbon +flights $420 +hotel $95 * 4 nights +food 30€ a day * 4 days and museums 25€ +total → $1,066.97 + +5 inches in cm → 12.7 cm +100 km/h in mph → 62.14 mph +$2000 in rubles → 144,060 ₽ +20% off 1500 → 1,200 +today + 6 weeks → 22 July 2026 +time in Tokyo → 14:25 +``` + +Works in **English and Russian** at the same time: `5 метров в см`, `20$ в евро`, +`сумма` — no language switching. + +## Install + +Grab the latest `Summarum_*_x64-setup.exe` from [Releases](../../releases), +run it, done. The whole app is about 2 MB. + +Requirements: + +- **Windows 10 (1803+) or Windows 11**, 64-bit. +- **[WebView2 Runtime](https://developer.microsoft.com/en-us/microsoft-edge/webview2/)** — + already part of Windows 11 and most updated Windows 10 systems; if it's + missing, the installer downloads it automatically. +- Internet connection is only needed for live currency rates — everything + else works offline (rates fall back to a cached/bundled snapshot). + +## What it understands + +**Plain math** — `2 + 2`, `8 / (45 - 20%)`, parentheses, `^`, `sqrt`, `sin`, +factorials. Decimal-exact: `0.1 + 0.2` is `0.3`, not `0.30000000000000004`. + +**Words as operators** — `12 plus 4`, `20 divided by 4`, `5 и 3`. Text around +the math is ignored, so `spent 20 on pizza and 5 on coffee` just gives 25. + +**Units** — length, weight, volume, area, temperature, time, angles, data +(`1 GiB in MB`), CSS px/em, speed, pressure, energy, power, frequency, fuel: + +``` +6 feet in meters 1 atm in mmHg 8 л/100км in mpg +2 hours 30 min in min 150 hp in kW 32 px in em +``` + +**Money** — ~150 currencies and the top-20 crypto with live rates +(refreshed hourly, cached for offline). `$100 + 20%`, `5 eth in USD`, +`9800 рублей в долларах`. + +**Percents the human way** — `20% of 80`, `20% off 1500`, `5 as a % of 25`, +`20% of what is 30`. + +**Dates and time** — `today + 2 weeks`, `tomorrow - today`, `time in New York`, +`now in unix`, `1750000000 as date`. + +**Programmer stuff** — `0xFF & 0x0F`, `1 << 4`, `255 in hex`, `in binary`, +plus `in fraction` (0.75 → 3/4) and `in roman` (2026 → MMXXVI). + +**Sheets that calculate** — variables (`rent = $500`), running totals +(`sum`, `avg`, `prev`), `#` headers, `//` comments. The status bar always shows +the sheet total; select a few lines to see just their sum. + +## Handy things + +- **Ctrl+Alt+N** (configurable — the settings field records keys you press) + shows/hides the window from anywhere. Closing hides to tray. +- **Click a result** to copy it. Copying a line copies it *with* the answer + (`rent * 3 = $1,500`). **Ctrl+Shift+C** copies the whole sheet with answers. +- **Drag the divider** to resize the results column. +- **Drop a `.numi` or `.txt` file** into the window to open it as a sheet. +- **Backups are automatic**: a daily snapshot of all sheets (last 14 kept), and + deleted sheets sit in a bin for 14 days (configurable) — restore one by + dragging the file back in. Settings → Backups opens the folder. +- **Data folder is configurable** — point it at OneDrive/Dropbox and your + sheets follow you between machines. +- Light/dark theme, en/ru interface, configurable precision and separators. + +## Extensions + +Drop a `.js` file into the extensions folder (Settings → Extensions folder) +and restart. The API is compatible with Numi extensions: + +```js +numi.setVariable("vat", { double: 20 }); +numi.addUnit({ id: "floor", phrases: "floor,floors", baseUnitId: "meter", + format: "fl", ratio: 3 }); +numi.addFunction({ id: "hyp", phrases: "hyp" }, + (v) => ({ double: Math.hypot(v[0].double, v[1].double) })); +``` + +Then `5 floors in meters` → `15 m` and `hyp(3; 4)` → `5`. + +## Bugs and ideas + +Open an [issue](../../issues). A line of text and what you expected is enough. + +## For developers + +Engine internals, project layout and how to add units/currencies: +see [ARCHITECTURE.md](ARCHITECTURE.md). + +```bash +npm install +npm test # engine tests +npm run tauri dev # run the app +npm run tauri build # build the installer +``` + +To build from source you need: + +- [Node.js](https://nodejs.org/) 20 or newer +- [Rust](https://rustup.rs/) (stable, MSVC toolchain) +- [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/) + with the "Desktop development with C++" workload (the MSVC linker) +- See the [Tauri prerequisites](https://tauri.app/start/prerequisites/) page + if anything refuses to compile + +Stack: [Tauri 2](https://tauri.app) + TypeScript + CodeMirror 6. + +## Credits + +Inspired by the excellent [Numi](https://numi.app) by Dmitry Nikolaev — +if you're on macOS, buy it. Summarum is an independent project, built from +scratch for Windows. + +## License + +[MIT](LICENSE) diff --git a/index.html b/index.html new file mode 100644 index 0000000..11d7326 --- /dev/null +++ b/index.html @@ -0,0 +1,98 @@ + + + + + + Summarum + + +
+ +
+
+ + + + +
+ + +
+
+
+
+
+
+
+
+ + + +
+
+ + + +
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..7c389b4 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2064 @@ +{ + "name": "numi-windows", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "numi-windows", + "version": "1.0.0", + "dependencies": { + "@codemirror/autocomplete": "^6.18.0", + "@codemirror/commands": "^6.8.0", + "@codemirror/language": "^6.10.0", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.36.0", + "@lezer/highlight": "^1.2.0", + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-autostart": "^2.3.0", + "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-global-shortcut": "^2.2.0", + "@tauri-apps/plugin-opener": "^2.2.0", + "decimal.js": "^10.4.3" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.5.0", + "@types/node": "^25.9.2", + "typescript": "^5.7.0", + "vite": "^6.0.0", + "vitest": "^3.0.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.3", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz", + "integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.43.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.1.tgz", + "integrity": "sha512-+BIjw/AG3tDQ4pJgTLPYdAW25eDE66YsvM4LKyVPgGzVgZ4a9Wj1SRX8kPVKgBDdPt8oHtZ15F0qx7p0oOHdHw==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tauri-apps/api": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.11.0.tgz", + "integrity": "sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==", + "license": "Apache-2.0 OR MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + } + }, + "node_modules/@tauri-apps/cli": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.11.2.tgz", + "integrity": "sha512-bk3HemqvGRoy+5D/dVMUQHKMYLglD0jVnMm/0iGMH6ufZ+p8r14m6BpIixwij3PBvZdvORUp1YifTD8QxVZ1Nw==", + "dev": true, + "license": "Apache-2.0 OR MIT", + "bin": { + "tauri": "tauri.js" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/tauri" + }, + "optionalDependencies": { + "@tauri-apps/cli-darwin-arm64": "2.11.2", + "@tauri-apps/cli-darwin-x64": "2.11.2", + "@tauri-apps/cli-linux-arm-gnueabihf": "2.11.2", + "@tauri-apps/cli-linux-arm64-gnu": "2.11.2", + "@tauri-apps/cli-linux-arm64-musl": "2.11.2", + "@tauri-apps/cli-linux-riscv64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-gnu": "2.11.2", + "@tauri-apps/cli-linux-x64-musl": "2.11.2", + "@tauri-apps/cli-win32-arm64-msvc": "2.11.2", + "@tauri-apps/cli-win32-ia32-msvc": "2.11.2", + "@tauri-apps/cli-win32-x64-msvc": "2.11.2" + } + }, + "node_modules/@tauri-apps/cli-darwin-arm64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.11.2.tgz", + "integrity": "sha512-+4UZzLt+eOAEQCwgd+TqKgyUJMrvx+BgdXLLaqJYmPqzP+nE6YZr/hY6CWLYGQb8jFn99jEkmC6uA3tNvamA1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-darwin-x64": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.11.2.tgz", + "integrity": "sha512-VjYYtZUPqDMLutSfJEyxFE3Bz+DPi7c8wC3imckgvciLDZLq4qwKJxBicg0BXGhXjJsl8vKWgWRFNMPELQ+Xyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.11.2.tgz", + "integrity": "sha512-yMemD6f4i95AQriS8EazyOFzbE34yjnP16i3IOzpHGQvBoy2DjypFMFBq0NtPuITURv/cOGguRtHR5d79/9CSA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.11.2.tgz", + "integrity": "sha512-cgI91D2wL8GSgoWwZXDqt+DwnuZCP2/bz03QAE4TrhgAKIsrB4hX26W/H1EONPUUNkqrsgeCD0wU6pcNjV/5kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-arm64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.11.2.tgz", + "integrity": "sha512-X1rm0BERqAAggtYTESSgXrS3sz4Sb/OiPiz54UqISlXW+GkR3vNIGnsy/lejNmoXGVqri3Q53BCfQiclOIyRPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-riscv64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-riscv64-gnu/-/cli-linux-riscv64-gnu-2.11.2.tgz", + "integrity": "sha512-usbMLJbT3KtkOrBMDVeGYNM35aTHXx38SJSzTMSqqjeUIOQ+iVPjb2yAGNAE+KqmBbAx4FOFIyMeKXx2M/JKGQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-gnu": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.11.2.tgz", + "integrity": "sha512-Ru4gwJKPG0ctVGchRGpRup4Y4lW2SSfFnrbQcyHhCliKy4g8Qz97TrUgCur4CbWyAgKxvGh3SjrkA0LDYzDGiw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-linux-x64-musl": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.11.2.tgz", + "integrity": "sha512-eUm7T6clN1MMmNSRQ9gaWsQdyehQx2Gmn5hht/QUlqZQI/qcP2OJK5dnaxqwFzCr2HdsEo9ydxaqcS1oJzMvUw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-arm64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.11.2.tgz", + "integrity": "sha512-HeeZW80jU+gVTOEX4X/hC6NVSAdDVXajwP5fxIZ/3z9WvUC7qrudX2GMTilYq6Dg0e0sk0XgsAJD1hZ5wPBXUA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-ia32-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.11.2.tgz", + "integrity": "sha512-YhjQNZcXfbkCLyazSv1nPnJ9iRFE1wm6kc51FDbU10/Dk09io+6PAGMLjkxnX2GdM0qMnDmTjstY8mTDVvtKeA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/cli-win32-x64-msvc": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.11.2.tgz", + "integrity": "sha512-d2JchlFIpZevZVReyqhQOekJmb1UH3rhZ5VX6sH3ty9ETE0TKQavpihvoScUXfKKpW6HZC0MrFGRU0ZtD+w3gA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 OR MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tauri-apps/plugin-autostart": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-autostart/-/plugin-autostart-2.5.1.tgz", + "integrity": "sha512-zS/xx7yzveCcotkA+8TqkI2lysmG2wvQXv2HGAVExITmnFfHAdj1arGsbbfs3o6EktRHf6l34pJxc3YGG2mg7w==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, + "node_modules/@tauri-apps/plugin-dialog": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-dialog/-/plugin-dialog-2.7.1.tgz", + "integrity": "sha512-OK1UBXYt+ojcmxMktzzuyonYIFta8CmAASpX+CA+DTGK24KlHjhYI6x2iOJ/TjZF4N7/ACK1oFmEOjIY9IhzOQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tauri-apps/plugin-global-shortcut": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-global-shortcut/-/plugin-global-shortcut-2.3.2.tgz", + "integrity": "sha512-UReHNXrLvpEjylE4jb4oCYiy96uRykPUthoCQCmRXYrd5hs5X9DrW+qOn7GLW57EJN4tdK8bgK5twBTz2NOxzA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@tauri-apps/plugin-opener": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-opener/-/plugin-opener-2.5.4.tgz", + "integrity": "sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.11.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..ca5d44e --- /dev/null +++ b/package.json @@ -0,0 +1,37 @@ +{ + "name": "summarum", + "private": true, + "version": "1.0.0", + "description": "Summarum — natural language calculator notebook for Windows", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "icons": "node scripts/gen-icon.ts", + "tauri": "tauri" + }, + "dependencies": { + "@codemirror/autocomplete": "^6.18.0", + "@codemirror/commands": "^6.8.0", + "@codemirror/language": "^6.10.0", + "@codemirror/state": "^6.5.0", + "@codemirror/view": "^6.36.0", + "@lezer/highlight": "^1.2.0", + "@tauri-apps/api": "^2.5.0", + "@tauri-apps/plugin-autostart": "^2.3.0", + "@tauri-apps/plugin-dialog": "^2.7.1", + "@tauri-apps/plugin-global-shortcut": "^2.2.0", + "@tauri-apps/plugin-opener": "^2.2.0", + "decimal.js": "^10.4.3" + }, + "devDependencies": { + "@tauri-apps/cli": "^2.5.0", + "@types/node": "^25.9.2", + "typescript": "^5.7.0", + "vite": "^6.0.0", + "vitest": "^3.0.0" + } +} diff --git a/scripts/gen-icon.ts b/scripts/gen-icon.ts new file mode 100644 index 0000000..04e878a --- /dev/null +++ b/scripts/gen-icon.ts @@ -0,0 +1,156 @@ +// Generates the icon set with no dependencies: a minimal PNG encoder draws +// a dark rounded square with an orange equals sign. Usage: node scripts/gen-icon.ts +import * as fs from "node:fs"; +import * as path from "node:path"; +import * as zlib from "node:zlib"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const outDir = path.join(root, "src-tauri/icons"); + +// ---- tiny PNG encoder (RGBA, no filters) + +const CRC_TABLE = new Int32Array(256).map((_, n) => { + let c = n; + for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + return c; +}); + +function crc32(buf: Buffer): number { + let c = ~0; + for (const b of buf) c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8); + return ~c >>> 0; +} + +function chunk(type: string, data: Buffer): Buffer { + const len = Buffer.alloc(4); + len.writeUInt32BE(data.length); + const body = Buffer.concat([Buffer.from(type, "ascii"), data]); + const crc = Buffer.alloc(4); + crc.writeUInt32BE(crc32(body)); + return Buffer.concat([len, body, crc]); +} + +function encodePng(size: number, rgba: Buffer): Buffer { + const ihdr = Buffer.alloc(13); + ihdr.writeUInt32BE(size, 0); + ihdr.writeUInt32BE(size, 4); + ihdr[8] = 8; // bit depth + ihdr[9] = 6; // RGBA + // scanlines with filter byte 0 + const raw = Buffer.alloc(size * (size * 4 + 1)); + for (let y = 0; y < size; y++) { + raw[y * (size * 4 + 1)] = 0; + rgba.copy(raw, y * (size * 4 + 1) + 1, y * size * 4, (y + 1) * size * 4); + } + return Buffer.concat([ + Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), + chunk("IHDR", ihdr), + chunk("IDAT", zlib.deflateSync(raw, { level: 9 })), + chunk("IEND", Buffer.alloc(0)), + ]); +} + +// ---- drawing: dark rounded square, orange "equals" bars +// 4x supersampling for smooth edges + +const BG = [0x24, 0x25, 0x2a]; // dark slate +const FG = [0xe8, 0x92, 0x3c]; // summarum orange + +function draw(size: number): Buffer { + const ss = 4; + const S = size * ss; + const corner = S * 0.22; + const px = Buffer.alloc(size * size * 4); + + const insideRoundRect = (x: number, y: number): boolean => { + const r = corner; + const cx = Math.max(r, Math.min(S - r, x)); + const cy = Math.max(r, Math.min(S - r, y)); + return (x - cx) ** 2 + (y - cy) ** 2 <= r * r; + }; + + // equals bars geometry (relative to S) + const barW = S * 0.52; + const barH = S * 0.115; + const barR = barH / 2; + const x0 = (S - barW) / 2; + const topY = S * 0.355; + const botY = S * 0.53; + + const insideBar = (x: number, y: number, by: number): boolean => { + if (x < x0 || x > x0 + barW || y < by || y > by + barH) return false; + const cx = Math.max(x0 + barR, Math.min(x0 + barW - barR, x)); + const cy = by + barR; + return (x - cx) ** 2 + (y - cy) ** 2 <= barR * barR || (x >= x0 + barR && x <= x0 + barW - barR); + }; + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + let bgHits = 0; + let fgHits = 0; + for (let sy = 0; sy < ss; sy++) { + for (let sx = 0; sx < ss; sx++) { + const X = x * ss + sx + 0.5; + const Y = y * ss + sy + 0.5; + if (!insideRoundRect(X, Y)) continue; + bgHits++; + if (insideBar(X, Y, topY) || insideBar(X, Y, botY)) fgHits++; + } + } + const total = ss * ss; + const alpha = Math.round((bgHits / total) * 255); + const fgRatio = bgHits > 0 ? fgHits / bgHits : 0; + const i = (y * size + x) * 4; + px[i] = Math.round(BG[0] + (FG[0] - BG[0]) * fgRatio); + px[i + 1] = Math.round(BG[1] + (FG[1] - BG[1]) * fgRatio); + px[i + 2] = Math.round(BG[2] + (FG[2] - BG[2]) * fgRatio); + px[i + 3] = alpha; + } + } + return px; +} + +fs.mkdirSync(outDir, { recursive: true }); +const pngs = new Map(); +for (const size of [16, 32, 48, 64, 128, 256, 512]) { + pngs.set(size, encodePng(size, draw(size))); +} + +const files: Array<[string, number]> = [ + ["32x32.png", 32], + ["128x128.png", 128], + ["128x128@2x.png", 256], + ["icon.png", 512], +]; +for (const [name, size] of files) { + fs.writeFileSync(path.join(outDir, name), pngs.get(size)!); + console.log(`${name} (${size}px)`); +} + +// .ico with PNG-compressed entries +function buildIco(sizes: number[]): Buffer { + const header = Buffer.alloc(6); + header.writeUInt16LE(1, 2); + header.writeUInt16LE(sizes.length, 4); + const dirs: Buffer[] = []; + const blobs: Buffer[] = []; + let offset = 6 + sizes.length * 16; + for (const s of sizes) { + const data = pngs.get(s)!; + const dir = Buffer.alloc(16); + dir.writeUInt8(s >= 256 ? 0 : s, 0); + dir.writeUInt8(s >= 256 ? 0 : s, 1); + dir.writeUInt16LE(1, 4); + dir.writeUInt16LE(32, 6); + dir.writeUInt32LE(data.length, 8); + dir.writeUInt32LE(offset, 12); + offset += data.length; + dirs.push(dir); + blobs.push(data); + } + return Buffer.concat([header, ...dirs, ...blobs]); +} + +fs.writeFileSync(path.join(outDir, "icon.ico"), buildIco([16, 32, 48, 64, 128, 256])); +console.log("icon.ico (6 sizes)"); diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock new file mode 100644 index 0000000..5d861e0 --- /dev/null +++ b/src-tauri/Cargo.lock @@ -0,0 +1,5745 @@ +# 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 = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[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.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +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 = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "brotli" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8119e4516436f5708bbc474a9d395bf12f1b5395e93a92a56e647ac3388c8610" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5962523e1b92ce1b5e793d9169b9943eece10d39f62550bc04bb605d75b94924" +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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[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 = "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.13.0", + "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.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +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.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link 0.2.1", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "064badf302c3194842cf2c5d61f56cc88e54a759313879cdf03abdd27d0c3b97" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-graphics-types", + "foreign-types 0.5.0", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.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 = "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 = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + +[[package]] +name = "dirs" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" +dependencies = [ + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.5.2", + "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.13.0", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +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 = "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 0.55.0", +] + +[[package]] +name = "embed_plist" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[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 = "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.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared 0.1.1", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared 0.3.1", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.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-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[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.13.0", + "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 = "global-hotkey" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c386b0a4a70cb2d39fffd74480f985b6f0bfbcb934b6a6b6b7e630e448f242e" +dependencies = [ + "crossbeam-channel", + "keyboard-types", + "objc2", + "objc2-app-kit", + "once_cell", + "serde", + "thiserror 2.0.18", + "windows-sys 0.59.0", + "x11rb", + "xkeysym", +] + +[[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 = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.14.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[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 = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.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", + "system-configuration", + "tokio", + "tower-service", + "tracing", + "windows-registry", +] + +[[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 = "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 = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + +[[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.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2025f20d7a4fa7785846e7b63d10a76d3f1cee98ee5cb79ea59703f95e42162" +dependencies = [ + "cfg-if", + "futures-util", + "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.13.0", + "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.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[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 = "native-tls" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.13.0", + "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 = "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.13.0", + "block2", + "objc2", + "objc2-core-foundation", + "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.13.0", + "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.13.0", + "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.13.0", + "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.13.0", + "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.13.0", + "block2", + "libc", + "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.13.0", + "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.13.0", + "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.13.0", + "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.13.0", + "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 = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "dunce", + "is-wsl", + "libc", + "pathdiff", +] + +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "foreign-types 0.3.2", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "pango" +version = "0.18.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4" +dependencies = [ + "gio", + "glib", + "libc", + "once_cell", + "pango-sys", +] + +[[package]] +name = "pango-sys" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5" +dependencies = [ + "glib-sys", + "gobject-sys", + "libc", + "system-deps", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.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 = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[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 = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[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.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[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.12+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 = "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.13.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +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.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "js-sys", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-native-tls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +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 = "rfd" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15ad77d9e70a92437d8f74c35d99b4e4691128df018833e99f90bcd36152672" +dependencies = [ + "block2", + "dispatch2", + "glib-sys", + "gobject-sys", + "gtk-sys", + "js-sys", + "log", + "objc2", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-foundation", + "raw-window-handle", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.60.2", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[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.13.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[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 = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "selectors" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5d9c0c92a92d33f08817311cf3f2c29a3538a8240e94a6a3c622ce652d7e00c" +dependencies = [ + "bitflags 2.13.0", + "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.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +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_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "3.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a5c54c7310e7b8b9577c286d7e399ddd876c3e12b3ed917a8aabc4b96e9e8c" +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.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d57bc0c8b9a17920c178daa6bb924850d54a9c97ab45194bb8c17ad66bb660" +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 = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +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 = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "summarum" +version = "1.0.0" +dependencies = [ + "chrono", + "reqwest 0.12.28", + "serde", + "serde_json", + "tauri", + "tauri-build", + "tauri-plugin-autostart", + "tauri-plugin-dialog", + "tauri-plugin-global-shortcut", + "tauri-plugin-opener", + "tauri-plugin-single-instance", +] + +[[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-configuration" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" +dependencies = [ + "bitflags 2.13.0", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-deps" +version = "6.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349" +dependencies = [ + "cfg-expr", + "heck 0.5.0", + "pkg-config", + "toml 0.8.2", + "version-compare", +] + +[[package]] +name = "tao" +version = "0.35.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1c93047acf68669466a34690ac58cca7010bd1b201e1ec86f1fd0a75d3dd4a9" +dependencies = [ + "bitflags 2.13.0", + "block2", + "core-foundation 0.10.1", + "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 6.0.0", + "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 0.13.4", + "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 6.0.0", + "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-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-dialog" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65981abb771e74e571a38196c3baa11c459379164791eba0e67abc1a5fac9884" +dependencies = [ + "log", + "raw-window-handle", + "rfd", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "tauri-plugin-fs", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "tauri-plugin-fs" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7ecc274121aca0c036a2b42d1cbe83d368d348f54e0bb8a735c2b1548e8f371" +dependencies = [ + "anyhow", + "dunce", + "glob", + "log", + "objc2-foundation", + "percent-encoding", + "schemars 0.8.22", + "serde", + "serde_json", + "serde_repr", + "tauri", + "tauri-plugin", + "tauri-utils", + "thiserror 2.0.18", + "toml 1.1.2+spec-1.1.0", + "url", +] + +[[package]] +name = "tauri-plugin-global-shortcut" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4dd9f4c5136c09cd962da0c86dc4accd4666db2ea591cf16e6597435843bd2b" +dependencies = [ + "global-hotkey", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", + "url", + "windows", + "zbus", +] + +[[package]] +name = "tauri-plugin-single-instance" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c8f29386f5e9fdc699182388a33ee80a56de436d91b67459e86afef426282af" +dependencies = [ + "serde", + "serde_json", + "tauri", + "thiserror 2.0.18", + "tracing", + "windows-sys 0.60.2", + "zbus", +] + +[[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 = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[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 = "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-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[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.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +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.13.0", + "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-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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 6.0.0", + "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 = "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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "144d6b123cef80b301b8f72a9e2ca4370ddec21950d0a103dd22c437006d2db7" +dependencies = [ + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version-compare" +version = "0.2.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.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a254a4b10c19a76f09a27640e7ffbf9bc30bf67e16a3bf28aaefa4920fe81563" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54568702fabf5d4849ce2b90fadfa64168a097eaf4b351ce9df8b687a0086aaf" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a40fc75b0ec6f3746ceb10d36f53a93dcd68a93b11b6445983945d79eba0dc" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "908f34bd9b9ce3d4caf07b72dfab63d61504d156856c6bd3cd87fa350cf3985b" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.123" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7acbf7616c27b194bbb550bf77ed0c2c3e5b7fd1260a93082b95fb7f47959b92" +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.13.0", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver", +] + +[[package]] +name = "web-sys" +version = "0.3.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0871acf327f283dc6da28a1696cdc64fb355ba9f935d052021fa77f35cce69" +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 = "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-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link 0.2.1", + "windows-result 0.4.1", + "windows-strings 0.5.1", +] + +[[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.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-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.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[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.13.0", + "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 = "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 6.0.0", + "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 = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +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 = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow 1.0.3", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow 1.0.3", + "zvariant", +] + +[[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 = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[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 = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow 1.0.3", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow 1.0.3", +] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml new file mode 100644 index 0000000..9a49c76 --- /dev/null +++ b/src-tauri/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "summarum" +version = "1.0.0" +description = "Summarum — natural language calculator notebook for Windows" +edition = "2021" + +[build-dependencies] +tauri-build = { version = "2", features = [] } + +[dependencies] +tauri = { version = "2", features = ["tray-icon"] } +tauri-plugin-global-shortcut = "2" +tauri-plugin-autostart = "2" +tauri-plugin-single-instance = "2" +tauri-plugin-opener = "2" +tauri-plugin-dialog = "2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.12", features = ["json"] } +chrono = "0.4" + +[profile.release] +strip = true +lto = true +opt-level = "s" +codegen-units = 1 diff --git a/src-tauri/build.rs b/src-tauri/build.rs new file mode 100644 index 0000000..d860e1e --- /dev/null +++ b/src-tauri/build.rs @@ -0,0 +1,3 @@ +fn main() { + tauri_build::build() +} diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..ae835de --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,25 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "default", + "description": "Main window capabilities", + "windows": ["main"], + "permissions": [ + "core:default", + "core:window:allow-show", + "core:window:allow-hide", + "core:window:allow-set-focus", + "core:window:allow-is-visible", + "core:window:allow-is-focused", + "core:window:allow-close", + "core:window:allow-minimize", + "core:window:allow-start-dragging", + "global-shortcut:allow-register", + "global-shortcut:allow-unregister", + "global-shortcut:allow-is-registered", + "autostart:allow-enable", + "autostart:allow-disable", + "autostart:allow-is-enabled", + "opener:default", + "dialog:default" + ] +} diff --git a/src-tauri/extension-sample.js b/src-tauri/extension-sample.js new file mode 100644 index 0000000..26dc6b4 --- /dev/null +++ b/src-tauri/extension-sample.js @@ -0,0 +1,21 @@ +// Summarum extension example. +// Drop .js files into this folder and restart the app. +// The API is compatible with Numi extensions: numi.setVariable, +// numi.addUnit, numi.addFunction. + +// A global variable usable on any sheet, e.g. `salary * vat` +// numi.setVariable("vat", { "double": 20 }); + +// A custom unit: 1 floor = 3 meters, so `5 floors in meters` works +// numi.addUnit({ +// "id": "floor", +// "phrases": "floor, floors, этаж, этажа, этажей", +// "baseUnitId": "meter", +// "format": "fl", +// "ratio": 3, +// }); + +// A custom function: hyp(3; 4) -> 5 +// numi.addFunction({ "id": "hyp", "phrases": "hyp" }, function (values) { +// return { "double": Math.hypot(values[0].double, values[1].double) }; +// }); diff --git a/src-tauri/icons/128x128.png b/src-tauri/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..793ee17fd53d11f45df1695e7ddbe85a59e88b17 GIT binary patch literal 971 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV4mdZ;uunK>+KxdzAFYI4aF@o zN|}e0n#2TlGimDbdO5qhx;J0Cs3_i1wq&mZ-v;&tJ_i}wq4@bg_w+DN;p4D>&TzuRtE4XSa>kYvd4{oki zbjZ>UV%o#IYvtC1SDw7*eD&;x|HK5ZPnuu0#VRbD&gF1E+(2lKaD%1&x>*U+7(#w~ zg|RQc_LD`&Z|NwkU>M+3tymI<$>Kly+3xz~Yg!5@D=dw)GSU1CKns{!v1*?-M9 zIInDBwb`b6Cek79OS$~KmEQZd%-r0-e)(YSXQf|9yrpaTe;>ThKA~5TGsvf<;KSio z0fYw=Bu^Mk=v6Rly1|vEJ>#$fU$X_@Gm9DAiZL7yy(Gd9N&MR>(#61W;IM#P+Xs`L z<%u?YPYX59AFh-;$FB6lfn|QjLt(=jm9BmlMsX(wMnX`bfRT^INta!4fwDl>OP7Xe z4l9<5GDrRQTVxM*_1%qF=g)aSGcjd( z-SSU0ehe!`zO&?ie$yWMUxqP4%5B?|-7XAnO&-<%WEkhLTw%FA>+@yJopZGqRyX&C z|Mup5@Qw3^4p;Hy<+Em=_hymElMYsB3w$xt`|}TdrX9Bq7${Y|{+RYEX8$w>=d9|} zErE%5@7>#VTE=_B5(a0l>f1ZEifSq!NwwXx*)S)^NBsQNeBI9yUQ>$>~^fItusPE=TMcNL|BT;%{^fx-%kMW|vTsIh9r3lbV2IpMMa zWmklDxu^&h6%~z+7la~7(N(azL|heA;0U6IYc{YX5J(#LL-#|!b^3mIf6qKK|Cx85 zdH!!v-1-PBb4POku!@RgZv;S$5&^oY5nB({?*gEmk79>x%6v7}yL!JzJacD%+}PP|I@&n-RC!F79P z>p8wLU{W@Kcnd{Z)Z>T z%;>k}ohjcBdR#E{Jm+#U8Or+bFU?VfoTF88ef>4u6Bj3kyx%a1u_@DgPj?sxGc!FN z|Df&}H{r=g*B*$uj zpO)7;crW2F^+qWgSNU8*yW+U1iN+oNs%_hOKhqi>W#R9dYwOFSVTJDJ+P`TSm#)&t z`)t>n{vnxN&mraw=35x`66LDGzOmk&qI%s;Io^2i;|2=3DtO0Sl$?~vt-E=*`wJxF zzJ8lvMRbm$!>#So7)w58ql0ud`}(ufN%1MliE#t}zQUX8I=DLf2h#r;s_q z_K868SE$$F)vV|nITS_XJ6h5zt7(lHk)w}O=}^s*-Z3;(|6rjRaadPtc`TViN`t9d z+fTRNR@GD~Vp=|W*6$9x^sM%y^5(|*23=T_4i%h4$-laKyxtKHqcakGf?hF~CodEC zX(aLwCca~|0Y45Cn5F}F>|l$CMZVxlgm}yi0gTQ|hzV1zexdjS(qIT9=fNFVDt3(- zP7k!MK)v`Ztggup=q)xFFakk1?v&DelLqJ_U;zH9Uey1apvuD&r2_R2znt70D>a-F zX3c9Rg0~tb^$}}I$CH}*N|*Cl%1V3U3Qg}C?8*^kYTn>0*v;k2YI8B{o0{FZlVrN& zrXLR)K~+Ul%r>lu)w{Q|1?DD3U#3|WbGw2xqZaZf%TFNI@y!~Pp>;J3Xp6**tpZaj zRxcCke#Gc2gD5CNq11j(A)J&+3|J(pVAv8K4Vw6VR}9wa2lo=dR!vV;N~*VXd9xs6 zemlPg=zwB?5PBFdIou zX*H5Y5iDbriBQH<9jDcYk^6g1UlIOexHoI}KrEa!*b|dT%3Z_6T9hhZ5E7s_CUj@) z!h|e6fm_QnoXZ+n%p|UamADsuVfX&-{qpfz@hZsbw5}Uks(u^L3V<44h5_+Opk(N; z{EzWTY@6RYfqTtAx_Ko5D;hlQA6ANko2a|7(RLazEACUjsM`HU-c zj4^VsMte{pH)KXAf(hv5prC4aUrHSgKWxH_^@*bH8To;V@x{4|c^N+e-dW zgcRQI5!LbJuZfVmzQPm+wY(ZZ=#TgQa~s8;AYnN>*SCzZ-(!XauTL#wyk|< z#;@~|!N-4F2q?h5wmCO=*4d@%!}c@EzH?c-NsHf7TKeB9{m;$_ z<$y92k1Y2D3nm)+M>bnnG1}6TVU$K-BZd*QWa}N@xSPjjD`ZYY zkp(sU+cZqP zMbv0T#3FH8jzNc#@nAdm($ETt)&$Da^Ud)7R%jRW%4D(NY>&=V^{#DgG$Z&_Y%<88 zGp#uk!utFVWypbTutVD_GKxKn^!}NZse+dmsrY&i$k0wdNSd=BNnFeE^5)?WWzRZYbXhqUkwlHzcDsTZ^}wnY3H~qL&zn1YT70^7 dP2c@pgtY(Sc*}E5^+zzCsPOgdtD)T7e*+D6K2`t# literal 0 HcmV?d00001 diff --git a/src-tauri/icons/32x32.png b/src-tauri/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..eedb8e2c7b5ef3159a73bc8cdc1e74a40f690aa8 GIT binary patch literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE1|*BCs=ffJ1D-C9Ar-fh0|KumIUHhg*yZRN z`g%{-s%!OHk3R=&W}WcXWt$C1%$8Tj?fCnJWeY778u`Q)KW3T1%<|$%agL(m0fye` z|Cw8)B{HTw=gTe~DWM4fFw$PQ literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.ico b/src-tauri/icons/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..85aff5b7713d67e8074907cebde72b4a20c65aff GIT binary patch literal 4273 zcmb_f2~-nFx32C461EuDAZt)z65N8!ARtS#2nZaPCoy>)82U)8Ou1^@!G zfJ_D$Qb9QYAP~z`>JNPila(L<7>pnK7EG3f0YH!+dL^d&-~d3;ANpfVCdgowur$|R zF9CoKA^?Xy*EuW6Ysq6~r8O>&>oLT71Z1Kt_SF|i{V>?K#?fwL>>GbiuNS+pERW@e zs=>Xo_Qz$&O|uC6IBU5h~7YkUHp2)a=UE*@+815+!NM4@je= zb@HO@HdVAsZ)o~o)?DP=rl2+-QD5$* zX^GSCW2Moh=F}9D#<@OxJ`O+dF(TB!fD4Y~Npb1gn!*3#9a@*KhzqvRJZ8!4gz&oA z1N-=_sFc^zk$#Oue%X52J2Q2ddoK&5_ZMkrG9D^k(6@n6C7skLjj(5)V|01o!1<^IdA zZoqu`JIApZ2aNBIZ-J-S*mUasaD1fDnv{x=GlaD5!)BN68=rLAyUM{os87ZU%{Wgyw%b
    $h7TG#w^HKQZ;R#sYEvW| z)q279?m!r*sad97GPU0OeT_0zulj4l2TN2>lowVg=Og2i@??uJ-PofD7*}Qb%d1oE zd~C*cm!VHTs{n|My{Ht{F|)B+_L$GPVyS+=tm`uP68xtAYXgm-&5hEp!FiI*(7xyg zB*dZ%%tWe;ewHbU%Uo-*b)|`+`N*fKjWP7hsY`M71S(XLN1ib{XzTQT`3g}ve#U>@ zDDx`?vIb!<`bz1jYMpcTQ&k?*ut z?g{U~Xbu09)*=(vdx+Y>&op{qVK{|AarC;o&WanY@l`fDl~7$n9R9@$-M#Eu_RD?8 z7%NsBuecqMm%+&vsS`xOph2RU%g7$w9WL&Qh8pWiKXHk@KrCo^R>ZYYxi8G zzlnSlD@MQ1REv$@V*qeDf6mk?>!Y0vH8dh!XThH41*sb3+c15plB-Djx=b=b0$tlzyQ`h}7>MCQQA z-7NV}EP2oNzBFCs$~u`Yy0KK(C@abX_=|wka(RQQ*?tb ztbQ>hXV<0#Q-K`<{^3DS#wI1bV8x5COD83ku4TV=IN@WFxNj9)M9>f_!?XW}mzO_BE0UiYa39SX^b^1oC&u{R3bCp@pKs|Ow=dElqM!kvJ(TxRdjGR4cdxQ&R^aySQJ!Xnj|@Xeq$^45 zlIy%KV|Byui8r!yB1XUXqFdwjf?AaD{BDw9!Sp>h?WB3)yS;?$Bg%5?-P8B;TQbOh zImn#$9a$CrH2l+o|mHSzd z`on3hEpd9!2n$3A{>-8GswRnxGLJbz6npF#{fCFTA0pxHmlBjq7Lr2-9noB*;=DSf z1()VJ;c0T|+gz0qa}~uXmuIR!fhe?jibX@#BAs6IWSF+d>*?P9BpPbK2x0icWol_5 znN5-IzzN!Yhvg1JAj1f$1!N4XFMXi~rR(0!JlehWs)rnx?iWy=Tt1$-4qQ|d;==^P z>`hV~XsedfjgPk-fm5nWf+b1kc&y`AT?QOn6W-pc zQ>QPyx7(UiYZ4mjPHBA+wp5_$dh!5w-`|Hac1giV{2t0X4pdZP0F%(OGhO1pezk;?~7W>A?)XN8+xxto8r0xxrQ_5&Hn7a_Qmt}r}SqQgbsL|Ufi z9Dl?#e6B_xUB61u^Z}yxb)WY5_+*$L%+S%@U1_U%^nr9%7|PIjN@!`{HF$ub zafs)uM6wVCHTJb^j=y~!09WQ|UJF5AiHLqYM-{GKg@Gkz_F5I!3;R9Yc-g4W3nKfJT5g5BAwLLOyt{edqq2np*!Ze2J=7YBhrQ){-x67fFgpz7(U!3Y0Yqqzk zG{W(ck{^y)r8vQi2onRh+9X`t&5s1{xu0dsM+hG*d?|n;`{9;4*v>NGy=1bJ5%=17Fg(jf$7Ds4c`2Wv%Tfp4 zEHDBFe8&Mv6sAcu)fXGpy0YBhg2YOUXe5E-4HXVcOhZ0Km)x(km%d$`?6w%?+|0hhP=UI z`4kX3F}-^?kD`|vdCd9?%s)IqX{H`F=-%7P0W)L6ufi0wm>t%_VFm8fMaLoK4Glt= zEYg>L5FJh-?}(ShQ!AR1#m}h3W!5;D+%_m$j6<~ty~#k8l}}bfX_ti?diANmM$)$z z1&;j5i2`=<_6}165IyS`Qv!$p+w5T!VEZeEOm%Pxz@C4U>g&Z1oz>G&iC*Ywx$KWL zkDOu|?G%n|)~i$*stP+Prx*&u#V;hEV8P^ev2UXpIn`Cza~OHH+SDNUg9kV()j$Q% zt~=7Pl`x)5vRfs=7`io5KGqbunOPbnJ)iJpJ_WtvkZp8$iTbBI_wq(dQ^5mH39%*BclRzPJns#-|@cuf{0J{-rb4QKzZ)O zLXj$PPSw_oo|Quas(d$364V-d&WzRs`2aILjU0(R(asxZ+r6Plh6+|?P}i!Q?>nob zSNPfQ%wW&?gkYY+Uo0*7zbyURmM^&dgZ79^9vvu9VEb)EfS<>Gmw{$~q61URP z`4EYFPQh(+QI;%^-T~z2?Z#j;Ozo0Ktbq}1(1mCIg`Xg5mr!~eGLP}1rVFB={00WO zInoCV6Hw|G=@&SA$1QWu>%UFsH)*4X74WNzLgkS#Y$L6A@hrR4uPF@$Ii?i(%*f~% zPL%@s2%#m8hiw2HZICfK+iM1H$U3BKXWdjnJs?k!uT%{4HUPDmSJFnXRT+hYBr$SG zZzFgZ-`&*Y0dzFrslx7+jW^}c7(7*AEfvd_9I!K3yy*nAeM z6_}VgWghvmzx(EFA&DDe!|k9(hCQl!6J=$)2s}EhKstS3JiBJ54VYUK8~n%H!Z{wm z5vDh9IU%D}N!i zIc6q})j1XqhQtJ;%}TV$&8&`>@yT@PmiQtw#6h_rj8_odUzM~E+Pc!0VH$+|-t@dx xPn?i+5!x!0^Ha~58vn4I!$uy9ziw%L%`xi|*B?E{f*yX;s-v$c{$Ky-{{^7~uJr%_ literal 0 HcmV?d00001 diff --git a/src-tauri/icons/icon.png b/src-tauri/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..60c2e497fb0a7d64d90cbd5227ea756826944535 GIT binary patch literal 4597 zcmb7Id0dlM);{;mDq8{~1QZj*5tPNCMMc59Dhk#Oog%1EqQMrcvWN>9BrmR2QK{Gw zsY+^F#SNoaWfa3ZRu+XKE(I#F3AiCBAhMHXZuINV-#6cU)6QS__2a;}p}|6WxiCFA)Yb2j z)q~tG4+^A?g}!xpa?8?VL$^hNBYUrCo&})>o^I>Pj~%8$5(R4>yyT{8i!XN0nKNSM z!U{un%AQjR*E_28g;T~4=z5=aqJ7o=Q~h7NHt16mwqk!8tRE1&6I*UH>aZEM{5<Joo7QzmP4k7A#|>L+nHOK-!z*s~cvqCKb-~ecpZEoevx4_HR28 z@;C#!avWrr1bpDg?sH;;t+ET4nl(D73Plj!@{(fp7?|d^ydN+zO165b`=)2!ALQDOLK%`5Qz^9g6 zYUfM2hDWDITXwR@g0!w>k9p+R*IAPZrE!zN(&U8l>Q1~~&3F{hC$Hs^gKOAa%7WK< z@urX(_1b=t9XRwOX}S_T?v*RV;2bv+&UJ8TAB1#UBv8ZRQc>$~v~iTrT8L`|y+Q(B zHukX0O1}$KgE!6BJk%5R3rA3`*tkQ(18VZo7XE@Vkygs1}A>x6KDG0qY!ji3^X z3qah*AotmZ*1AfJI%roI9x~`RYDkEBR>lrDEEnQsYxYy$JtUM*&Za@a*_5zcyZkY5 z(wGOTtW{%>xTB0oO>z1$5YF{-D0Zurs)+~VBJGpQ!7Q~Fd#J*YxI36ZEy}33gFuBB zc!XVeiTH9yM`I|2o|*aU3r1ZV51fwkq!mJlDfClS2Ai(v5#4Z=y)nlg78@Ak-i@q? zE^O0kk)Y$j<-0|#An11k(tGCy7PUhG)%RKH4n{=ov?SWz{vdH24$YfBSqYQg@E~XT z(lDs&=76`>p>P3IM#e(ypSr@&+#>gX;ESYdqgcL5_?#OX{>G2hLBpu^QG#egq-YU-%&18wrfmJJU}TP~PrG zm6-!tO?^Ws+omIOQU7%&^^*cb-dRzEw85-#xXPjmOh`2E@meHapAMqgKdnX50h~LP zOLZAGU8^j<>)Z0QY;MAOg7HelWtIdr zD`kr@wy&Y_Wyjn0XB`z~Z=V(Crm<~Cwq7i(Z9KkLepj#^lpV8859(i36e~>klvaLw zhXI>H0mn}qFSuzmB<*SvFT z7wKGkh*@8GA)ObI7^wo$;M`qftvAyXeMbp~vS7Eo>s2Mii5x83=DP?oRXoQ9)*ER* z-w6UY77QBbc%za2K8Ycs^TQ?axsjo^rGUPzpt6uHOww}_Jop5~e%m0_UEwjEJn_PD zG`i}$*aBCS2&bKki^)7v??yo85j(;oHXwk5kPR^%LXX|P?#T9+ekX9t_?n=YB|U2E zb@V9TSpu`i&0CD+BF-9n+4hS#&OC)tf1CV0GN}vb3=)Dm6xlg^=qqaViZ$X{7Le*7 zXm^q_;dF}W`R-}F-chAY*>@mfJTBPfz#y~^rjl&w8hc2#fZnbHAX*tc_4rV5mHDy- zmMpMg6T|e|K;)>zXzS=I9WJViiK<1NtXhEXf9^+zz8>syFYx-t@ zbfL~|0U>ILHaZwwp?xw0bO+;ej649**4x*sJ3j9Sh6MLwAzt-ll$TqyP_a;BaKG#`+UYn>;vC#X9$+|R!v+;8BW@NkrrDi8ZcJ*j=F=NDEO!QSOL~kBxHyBlKydF|E9< z2xLM+l$2Onh-^Cs|4$t_jqM-LpRV?@um_)@lSMt87L>7rpek>jnrTgu8LZMEM0qQy z^XXjB_j3YRn1M6iF$X7gux#tag^*d;ySNbX3dfYPW#5Bnxw?b1rvD428HD1!CTiYZ zwC^hn4HK@!>5#d{jLUWOtoLB}&BUldcwfIr=y1XoE+0fTh5IlpM3~|K1zQM%&x|oM z2;YOT*GjA_pt&%N$(+RHA{uSM#q{1iif2NVJ{U5K%=|E`oYb_G z1~6d+%r$a>kGrj4kj?|$F-!pZ{gzi|@*frda@V(*3*D35X-^c}=B60X+k$3b6&24R zt;^N)~>z%N^{3Gap*QE$QbSeAa zbtyqG$wmMj9nv9?n96zmrPL62&R$*%6fhdD`{4e^1pyf)BvuSgm3S(yb@4Hd&jZpNe!t z5rpO*tI|t?VEeZcfQKuRbN%&d)UND(u97g}lh4z%eb+NU<;@3Y_3Ji_#c3kAEN#%b z67*IQVta2E&tQ^G13v*ht*avEYK0ZJGYPt3ZpG1p3L7ff>tcH_K`rvWv$<1&pE8~R z&V|jzd)`Y|nv2gs0*T37s}FtK>B`WR*Wi1r_HV`CO<-eUg=$Q&8?M%&)J(;(Lj-*SZz}O#^fAQ0HUc2-sQ!9b)$K42bqsIt%^uT9 z?4(!5pu@LKe%^SEAelhU%Thc>{GG;Li1{Mo#jwatuzj})cg>$I>e1J4$E*X{`tjEI z5r^M$k_Y$^$H#BEan@i9)NTAb!o}g~EfOBJUfMq+?a?sUZ_O6%xLsWTFy+c^r0Sg&^N`cCy~1l_3`$=sCva|5Bs0Yn33hQG7vel&ff zC8*YJFfH_XyJ9|4si#VxOD4mPC1=|DSeQTQTVo-=$^5T@h6}%D+shO+Z*!qHZ695i zkEodNt^-M%%f>=?y`MN?Kak@FdO7~*njK+RN}%pEkk`{*%PYsi$KzG|_{ul+z3k_n zD4{8wxdN2_S?XXtq3jSeIUdMyhzp{&Ewh)!X}=!}IafZDM%bU;z<_5(cXJ(NXE*f| z%CKo-k1NOP!a!Aj#*pZ{F{hnH9$Y9MGyhvVHkm)za86)3ubU7JbTMx<-l)kFkB-Y6 va$yK{Qj#AnEEu)=$&zBhlcKBfUz&)cXX3UC3Z7y_1I(T|KPZ2?bn8C>qZMKZ literal 0 HcmV?d00001 diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs new file mode 100644 index 0000000..1fdf402 --- /dev/null +++ b/src-tauri/src/main.rs @@ -0,0 +1,475 @@ +// Summarum — Tauri shell: tray, hide-to-tray window, app-data storage, +// currency rates fetching with an on-disk cache, extension script loading. +#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] + +use serde::{Deserialize, Serialize}; +use std::fs; +use std::path::PathBuf; +use std::time::{SystemTime, UNIX_EPOCH}; +use tauri::menu::{Menu, MenuItem}; +use tauri::tray::TrayIconBuilder; +use tauri::{AppHandle, Emitter, Manager, WindowEvent}; + +const RATES_TTL_SECS: u64 = 3600; +const SAMPLE_JS: &str = include_str!("../extension-sample.js"); + +fn data_dir(app: &AppHandle) -> PathBuf { + let dir = app + .path() + .app_data_dir() + .expect("no app data dir"); + fs::create_dir_all(&dir).ok(); + dir +} + +/// Documents live either in the default app-data dir or in a user-chosen +/// folder (settings stay in app-data — the path itself is stored there). +fn docs_dir(app: &AppHandle, dir: &Option) -> PathBuf { + match dir { + Some(d) if !d.trim().is_empty() => { + let p = PathBuf::from(d); + fs::create_dir_all(&p).ok(); + p + } + _ => data_dir(app), + } +} + +fn safe_name(name: &str) -> bool { + !name.is_empty() && !name.contains(['/', '\\', ':']) && !name.contains("..") +} + +#[tauri::command] +fn save_file(app: AppHandle, name: String, contents: String) -> Result<(), String> { + if !safe_name(&name) { + return Err("bad file name".into()); + } + fs::write(data_dir(&app).join(name), contents).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn load_file(app: AppHandle, name: String) -> Result, String> { + if !safe_name(&name) { + return Err("bad file name".into()); + } + let path = data_dir(&app).join(name); + if !path.exists() { + return Ok(None); + } + fs::read_to_string(path).map(Some).map_err(|e| e.to_string()) +} + +#[derive(Serialize, Deserialize, Clone)] +struct RatesPayload { + date: String, + rates: serde_json::Map, + #[serde(rename = "fetchedAt", default)] + fetched_at: u64, +} + +/// top cryptocurrencies fetched from CoinGecko (code, gecko id) +const CRYPTO: &[(&str, &str)] = &[ + ("BTC", "bitcoin"), ("ETH", "ethereum"), ("SOL", "solana"), ("BNB", "binancecoin"), + ("XRP", "ripple"), ("ADA", "cardano"), ("DOGE", "dogecoin"), ("TRX", "tron"), + ("TON", "the-open-network"), ("DOT", "polkadot"), ("LTC", "litecoin"), + ("AVAX", "avalanche-2"), ("LINK", "chainlink"), ("UNI", "uniswap"), ("XLM", "stellar"), + ("XMR", "monero"), ("ATOM", "cosmos"), ("BCH", "bitcoin-cash"), ("NEAR", "near"), + ("USDT", "tether"), ("USDC", "usd-coin"), +]; + +#[derive(Serialize, Deserialize)] +struct RatesCache { + fetched_at: u64, + payload: RatesPayload, +} + +fn now_secs() -> u64 { + SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() +} + +fn read_rates_cache(app: &AppHandle) -> Option { + let raw = fs::read_to_string(data_dir(app).join("rates.json")).ok()?; + serde_json::from_str(&raw).ok() +} + +async fn fetch_rates_online() -> Result { + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(15)) + .build() + .map_err(|e| e.to_string())?; + + let body: serde_json::Value = client + .get("https://open.er-api.com/v6/latest/USD") + .send() + .await + .map_err(|e| e.to_string())? + .json() + .await + .map_err(|e| e.to_string())?; + + if body["result"] != "success" { + return Err("rates api error".into()); + } + let mut rates = body["rates"].as_object().cloned().ok_or("no rates")?; + + // crypto via CoinGecko (er-api has no crypto); best-effort + let ids: Vec<&str> = CRYPTO.iter().map(|(_, id)| *id).collect(); + let url = format!( + "https://api.coingecko.com/api/v3/simple/price?ids={}&vs_currencies=usd", + ids.join(",") + ); + if let Ok(resp) = client.get(&url).send().await { + if let Ok(json) = resp.json::().await { + for (code, id) in CRYPTO { + if let Some(price) = json[id]["usd"].as_f64() { + if price > 0.0 { + rates.insert((*code).into(), serde_json::json!(1.0 / price)); + } + } + } + } + } + + Ok(RatesPayload { + date: body["time_last_update_utc"].as_str().unwrap_or("").to_string(), + rates, + fetched_at: now_secs(), + }) +} + +#[tauri::command] +async fn fetch_rates(app: AppHandle, force: bool) -> Result { + if !force { + if let Some(cache) = read_rates_cache(&app) { + if now_secs().saturating_sub(cache.fetched_at) < RATES_TTL_SECS { + let mut payload = cache.payload; + payload.fetched_at = cache.fetched_at; + return Ok(payload); + } + } + } + match fetch_rates_online().await { + Ok(payload) => { + let cache = RatesCache { fetched_at: now_secs(), payload: payload.clone() }; + if let Ok(raw) = serde_json::to_string(&cache) { + fs::write(data_dir(&app).join("rates.json"), raw).ok(); + } + Ok(payload) + } + Err(e) => read_rates_cache(&app) + .map(|c| { + let mut payload = c.payload; + payload.fetched_at = c.fetched_at; + payload + }) + .ok_or(e), + } +} + +// ---------- documents in the (possibly custom) data folder + +#[tauri::command] +fn load_documents(app: AppHandle, dir: Option) -> Result, String> { + let path = docs_dir(&app, &dir).join("documents.json"); + if !path.exists() { + return Ok(None); + } + fs::read_to_string(path).map(Some).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn save_documents(app: AppHandle, dir: Option, contents: String) -> Result<(), String> { + fs::write(docs_dir(&app, &dir).join("documents.json"), contents).map_err(|e| e.to_string()) +} + +// ---------- backups + +const SNAPSHOT_KEEP: usize = 14; + +/// Daily snapshot of documents.json + retention pruning. Runs at startup +/// before the app writes anything, so it captures yesterday's state. +#[tauri::command] +fn run_backups(app: AppHandle, dir: Option, retention_days: u32) -> Result<(), String> { + let base = docs_dir(&app, &dir); + let docs = base.join("documents.json"); + let bdir = base.join("backups"); + fs::create_dir_all(bdir.join("deleted")).map_err(|e| e.to_string())?; + + if docs.exists() { + let name = format!("documents-{}.json", chrono::Local::now().format("%Y-%m-%d")); + let snapshot = bdir.join(&name); + if !snapshot.exists() { + fs::copy(&docs, &snapshot).map_err(|e| e.to_string())?; + } + } + + // prune snapshots: keep the newest SNAPSHOT_KEEP by name (dates sort lexically) + let mut snaps: Vec = fs::read_dir(&bdir) + .map_err(|e| e.to_string())? + .flatten() + .map(|e| e.path()) + .filter(|p| { + p.is_file() + && p.file_name() + .and_then(|n| n.to_str()) + .is_some_and(|n| n.starts_with("documents-") && n.ends_with(".json")) + }) + .collect(); + snaps.sort(); + if snaps.len() > SNAPSHOT_KEEP { + for old in &snaps[..snaps.len() - SNAPSHOT_KEEP] { + fs::remove_file(old).ok(); + } + } + + // prune the deleted-sheets bin by age + let cutoff = SystemTime::now() - std::time::Duration::from_secs(u64::from(retention_days) * 86400); + if let Ok(entries) = fs::read_dir(bdir.join("deleted")) { + for entry in entries.flatten() { + if let Ok(meta) = entry.metadata() { + if meta.modified().map(|m| m < cutoff).unwrap_or(false) { + fs::remove_file(entry.path()).ok(); + } + } + } + } + Ok(()) +} + +/// A deleted sheet goes to the bin as a .numi file (restore = drag it back). +#[tauri::command] +fn backup_deleted_sheet(app: AppHandle, dir: Option, title: String, contents: String) -> Result<(), String> { + let bin = docs_dir(&app, &dir).join("backups").join("deleted"); + fs::create_dir_all(&bin).map_err(|e| e.to_string())?; + let safe: String = title + .chars() + .filter(|c| c.is_alphanumeric() || *c == ' ' || *c == '-' || *c == '_') + .take(40) + .collect(); + let safe = if safe.trim().is_empty() { "sheet".to_string() } else { safe.trim().to_string() }; + let name = format!("{}-{}.numi", safe, chrono::Local::now().format("%Y-%m-%d-%H%M%S")); + fs::write(bin.join(name), contents).map_err(|e| e.to_string()) +} + +#[tauri::command] +fn open_backups_folder(app: AppHandle, dir: Option) -> Result<(), String> { + let bdir = docs_dir(&app, &dir).join("backups"); + fs::create_dir_all(&bdir).ok(); + std::process::Command::new("explorer") + .arg(bdir) + .spawn() + .map_err(|e| e.to_string())?; + Ok(()) +} + +// ---------- data folder migration + +#[tauri::command] +fn data_dir_has_documents(dir: String) -> bool { + PathBuf::from(dir).join("documents.json").exists() +} + +/// strategy: "move" (copy mine, delete originals), "overwrite" (same, target +/// had documents), "use_existing" (just switch — keep the target's files) +#[tauri::command] +fn migrate_data_dir(app: AppHandle, old_dir: Option, new_dir: String, strategy: String) -> Result<(), String> { + let old = docs_dir(&app, &old_dir); + let new = PathBuf::from(&new_dir); + fs::create_dir_all(&new).map_err(|e| e.to_string())?; + if old == new { + return Ok(()); + } + if strategy == "use_existing" { + return Ok(()); + } + let old_docs = old.join("documents.json"); + if old_docs.exists() { + fs::copy(&old_docs, new.join("documents.json")).map_err(|e| e.to_string())?; + } + // merge backups (skip files that already exist in the target) + for sub in ["backups", "backups/deleted"] { + let from = old.join(sub); + let to = new.join(sub); + fs::create_dir_all(&to).ok(); + if let Ok(entries) = fs::read_dir(&from) { + for entry in entries.flatten() { + let dest = to.join(entry.file_name()); + if entry.path().is_file() && !dest.exists() { + fs::copy(entry.path(), dest).ok(); + } + } + } + } + // originals removed only after everything is copied + if old_docs.exists() { + fs::remove_file(&old_docs).ok(); + } + fs::remove_dir_all(old.join("backups")).ok(); + Ok(()) +} + +/// Used by drag&drop: read a calculation/text file from an absolute path. +#[tauri::command] +fn read_text_file(path: String) -> Result { + let p = std::path::PathBuf::from(&path); + let ext = p + .extension() + .and_then(|e| e.to_str()) + .unwrap_or("") + .to_lowercase(); + if !["numi", "txt", "md"].contains(&ext.as_str()) { + return Err("unsupported file type".into()); + } + let meta = fs::metadata(&p).map_err(|e| e.to_string())?; + if meta.len() > 1_000_000 { + return Err("file too large".into()); + } + fs::read_to_string(p).map_err(|e| e.to_string()) +} + +#[derive(Serialize)] +struct ExtensionScript { + name: String, + code: String, +} + +fn extensions_dir(app: &AppHandle) -> PathBuf { + let dir = data_dir(app).join("extensions"); + if fs::create_dir_all(&dir).is_ok() { + // ship Sample.js from the macOS bundle as the starter example + let sample = dir.join("Sample.js"); + if !sample.exists() { + fs::write(sample, SAMPLE_JS).ok(); + } + } + dir +} + +#[tauri::command] +fn load_extensions(app: AppHandle) -> Vec { + let mut out = Vec::new(); + if let Ok(entries) = fs::read_dir(extensions_dir(&app)) { + for entry in entries.flatten() { + let path = entry.path(); + if path.extension().is_some_and(|e| e == "js") { + if let Ok(code) = fs::read_to_string(&path) { + out.push(ExtensionScript { + name: entry.file_name().to_string_lossy().into_owned(), + code, + }); + } + } + } + } + out.sort_by(|a, b| a.name.cmp(&b.name)); + out +} + +/// Contents of a .numi file passed on the command line (file association). +#[tauri::command] +fn get_launch_file() -> Option { + let arg = std::env::args().nth(1)?; + if arg.to_lowercase().ends_with(".numi") { + fs::read_to_string(arg).ok() + } else { + None + } +} + +#[tauri::command] +fn open_extensions_folder(app: AppHandle) -> Result<(), String> { + let dir = extensions_dir(&app); + std::process::Command::new("explorer") + .arg(dir) + .spawn() + .map_err(|e| e.to_string())?; + Ok(()) +} + +fn toggle_window(app: &AppHandle) { + if let Some(win) = app.get_webview_window("main") { + let visible = win.is_visible().unwrap_or(false); + if visible && win.is_focused().unwrap_or(false) { + win.hide().ok(); + } else { + win.show().ok(); + win.set_focus().ok(); + } + } +} + +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_single_instance::init(|app, args, _cwd| { + if let Some(path) = args.iter().skip(1).find(|a| a.to_lowercase().ends_with(".numi")) { + if let Ok(content) = fs::read_to_string(path) { + app.emit("open-file", content).ok(); + } + } + if let Some(win) = app.get_webview_window("main") { + win.show().ok(); + win.set_focus().ok(); + } + })) + .plugin(tauri_plugin_global_shortcut::Builder::new().build()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + None, + )) + .plugin(tauri_plugin_opener::init()) + .plugin(tauri_plugin_dialog::init()) + .invoke_handler(tauri::generate_handler![ + save_file, + load_file, + load_documents, + save_documents, + run_backups, + backup_deleted_sheet, + open_backups_folder, + data_dir_has_documents, + migrate_data_dir, + fetch_rates, + load_extensions, + open_extensions_folder, + get_launch_file, + read_text_file + ]) + .setup(|app| { + let handle = app.handle().clone(); + let show = MenuItem::with_id(app, "show", "Show / Hide", true, None::<&str>)?; + let quit = MenuItem::with_id(app, "quit", "Quit Summarum", true, None::<&str>)?; + let menu = Menu::with_items(app, &[&show, &quit])?; + TrayIconBuilder::with_id("tray") + .icon(app.default_window_icon().unwrap().clone()) + .tooltip("Summarum") + .menu(&menu) + .show_menu_on_left_click(false) + .on_menu_event(move |app, event| match event.id.as_ref() { + "show" => toggle_window(app), + "quit" => app.exit(0), + _ => {} + }) + .on_tray_icon_event(|tray, event| { + if let tauri::tray::TrayIconEvent::Click { + button: tauri::tray::MouseButton::Left, + button_state: tauri::tray::MouseButtonState::Up, + .. + } = event + { + toggle_window(tray.app_handle()); + } + }) + .build(app)?; + let _ = handle; + Ok(()) + }) + .on_window_event(|window, event| { + // close hides to tray + if let WindowEvent::CloseRequested { api, .. } = event { + window.hide().ok(); + api.prevent_close(); + } + }) + .run(tauri::generate_context!()) + .expect("error while running Summarum"); +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json new file mode 100644 index 0000000..ce89525 --- /dev/null +++ b/src-tauri/tauri.conf.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://schema.tauri.app/config/2", + "productName": "Summarum", + "version": "1.0.0", + "identifier": "app.summarum.calc", + "build": { + "beforeDevCommand": "npm run dev", + "devUrl": "http://localhost:1420", + "beforeBuildCommand": "npm run build", + "frontendDist": "../dist" + }, + "app": { + "windows": [ + { + "title": "Summarum", + "width": 480, + "height": 620, + "minWidth": 320, + "minHeight": 240, + "center": true, + "visible": true, + "decorations": false + } + ], + "security": { + "csp": null + } + }, + "bundle": { + "active": true, + "targets": ["nsis"], + "icon": [ + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.png", + "icons/icon.ico" + ], + "fileAssociations": [ + { + "ext": ["numi", "sum"], + "name": "Summarum Sheet", + "description": "Summarum calculation sheet", + "role": "Editor" + } + ], + "windows": { + "nsis": { + "installMode": "currentUser", + "languages": ["English", "Russian"] + } + } + } +} diff --git a/src/engine/datetime.ts b/src/engine/datetime.ts new file mode 100644 index 0000000..ef3c0d6 --- /dev/null +++ b/src/engine/datetime.ts @@ -0,0 +1,109 @@ +// Date helpers and a city → IANA timezone map for "time in Tokyo". + +const Z: Record = { + // ---- common abbreviations + utc: "UTC", gmt: "UTC", est: "America/New_York", edt: "America/New_York", + cst: "America/Chicago", cdt: "America/Chicago", mst: "America/Denver", + pst: "America/Los_Angeles", pdt: "America/Los_Angeles", msk: "Europe/Moscow", + cet: "Europe/Paris", eet: "Europe/Kyiv", jst: "Asia/Tokyo", ist: "Asia/Kolkata", + // ---- cities (en) + "new york": "America/New_York", "los angeles": "America/Los_Angeles", + "san francisco": "America/Los_Angeles", chicago: "America/Chicago", + seattle: "America/Los_Angeles", boston: "America/New_York", miami: "America/New_York", + toronto: "America/Toronto", vancouver: "America/Vancouver", + "mexico city": "America/Mexico_City", "sao paulo": "America/Sao_Paulo", + "buenos aires": "America/Argentina/Buenos_Aires", lima: "America/Lima", + london: "Europe/London", dublin: "Europe/Dublin", lisbon: "Europe/Lisbon", + madrid: "Europe/Madrid", barcelona: "Europe/Madrid", paris: "Europe/Paris", + berlin: "Europe/Berlin", munich: "Europe/Berlin", amsterdam: "Europe/Amsterdam", + brussels: "Europe/Brussels", zurich: "Europe/Zurich", geneva: "Europe/Zurich", + rome: "Europe/Rome", milan: "Europe/Rome", vienna: "Europe/Vienna", + prague: "Europe/Prague", warsaw: "Europe/Warsaw", stockholm: "Europe/Stockholm", + oslo: "Europe/Oslo", copenhagen: "Europe/Copenhagen", helsinki: "Europe/Helsinki", + athens: "Europe/Athens", istanbul: "Europe/Istanbul", kyiv: "Europe/Kyiv", + kiev: "Europe/Kyiv", minsk: "Europe/Minsk", moscow: "Europe/Moscow", + "saint petersburg": "Europe/Moscow", riga: "Europe/Riga", vilnius: "Europe/Vilnius", + tallinn: "Europe/Tallinn", tbilisi: "Asia/Tbilisi", yerevan: "Asia/Yerevan", + baku: "Asia/Baku", dubai: "Asia/Dubai", "abu dhabi": "Asia/Dubai", + "tel aviv": "Asia/Jerusalem", jerusalem: "Asia/Jerusalem", riyadh: "Asia/Riyadh", + doha: "Asia/Qatar", tehran: "Asia/Tehran", karachi: "Asia/Karachi", + delhi: "Asia/Kolkata", mumbai: "Asia/Kolkata", bangalore: "Asia/Kolkata", + kolkata: "Asia/Kolkata", dhaka: "Asia/Dhaka", bangkok: "Asia/Bangkok", + hanoi: "Asia/Ho_Chi_Minh", "ho chi minh": "Asia/Ho_Chi_Minh", + jakarta: "Asia/Jakarta", singapore: "Asia/Singapore", "kuala lumpur": "Asia/Kuala_Lumpur", + manila: "Asia/Manila", "hong kong": "Asia/Hong_Kong", taipei: "Asia/Taipei", + beijing: "Asia/Shanghai", shanghai: "Asia/Shanghai", shenzhen: "Asia/Shanghai", + seoul: "Asia/Seoul", tokyo: "Asia/Tokyo", osaka: "Asia/Tokyo", + sydney: "Australia/Sydney", melbourne: "Australia/Melbourne", + brisbane: "Australia/Brisbane", perth: "Australia/Perth", auckland: "Pacific/Auckland", + cairo: "Africa/Cairo", lagos: "Africa/Lagos", nairobi: "Africa/Nairobi", + johannesburg: "Africa/Johannesburg", "cape town": "Africa/Johannesburg", + casablanca: "Africa/Casablanca", almaty: "Asia/Almaty", astana: "Asia/Almaty", + tashkent: "Asia/Tashkent", bishkek: "Asia/Bishkek", novosibirsk: "Asia/Novosibirsk", + yekaterinburg: "Asia/Yekaterinburg", vladivostok: "Asia/Vladivostok", + // ---- cities (ru) + "нью йорк": "America/New_York", "лос анджелес": "America/Los_Angeles", + "сан франциско": "America/Los_Angeles", чикаго: "America/Chicago", + торонто: "America/Toronto", лондон: "Europe/London", париж: "Europe/Paris", + берлин: "Europe/Berlin", мадрид: "Europe/Madrid", рим: "Europe/Rome", + амстердам: "Europe/Amsterdam", вена: "Europe/Vienna", прага: "Europe/Prague", + варшава: "Europe/Warsaw", стокгольм: "Europe/Stockholm", хельсинки: "Europe/Helsinki", + афины: "Europe/Athens", стамбул: "Europe/Istanbul", киев: "Europe/Kyiv", + минск: "Europe/Minsk", москва: "Europe/Moscow", москве: "Europe/Moscow", + питер: "Europe/Moscow", "санкт петербург": "Europe/Moscow", петербург: "Europe/Moscow", + рига: "Europe/Riga", вильнюс: "Europe/Vilnius", таллин: "Europe/Tallinn", + тбилиси: "Asia/Tbilisi", ереван: "Asia/Yerevan", баку: "Asia/Baku", + дубай: "Asia/Dubai", дубае: "Asia/Dubai", "тель авив": "Asia/Jerusalem", + дели: "Asia/Kolkata", мумбаи: "Asia/Kolkata", бангкок: "Asia/Bangkok", + бангкоке: "Asia/Bangkok", сингапур: "Asia/Singapore", сингапуре: "Asia/Singapore", + джакарта: "Asia/Jakarta", гонконг: "Asia/Hong_Kong", пекин: "Asia/Shanghai", + шанхай: "Asia/Shanghai", сеул: "Asia/Seoul", токио: "Asia/Tokyo", + сидней: "Australia/Sydney", мельбурн: "Australia/Melbourne", окленд: "Pacific/Auckland", + каир: "Africa/Cairo", алматы: "Asia/Almaty", астана: "Asia/Almaty", + ташкент: "Asia/Tashkent", бишкек: "Asia/Bishkek", новосибирск: "Asia/Novosibirsk", + екатеринбург: "Asia/Yekaterinburg", владивосток: "Asia/Vladivostok", +}; + +/** Resolve a sequence of words to an IANA timezone. Tries longest run first. */ +export function resolveZone(words: string[]): string | null { + const lower = words.map((w) => w.toLowerCase().replace(/[-.]/g, " ").trim()); + if (lower[0] === "local") return Intl.DateTimeFormat().resolvedOptions().timeZone; + for (let len = Math.min(lower.length, 3); len >= 1; len--) { + const key = lower.slice(0, len).join(" "); + if (Z[key]) return Z[key]; + } + // direct IANA id like Europe/Berlin typed by hand + const joined = words.join("/"); + try { + new Intl.DateTimeFormat("en", { timeZone: joined }); + return joined; + } catch { + return null; + } +} + +export function startOfToday(): number { + const d = new Date(); + d.setHours(0, 0, 0, 0); + return d.getTime(); +} + +/** Calendar-aware add: months/years via Date methods, the rest via ms. */ +export function addToDate(ms: number, amount: number, unitId: string): number { + const baseId = unitId.includes(":") ? unitId.split(":")[1] : unitId; + const d = new Date(ms); + if (baseId === "month" && Number.isInteger(amount)) { + d.setMonth(d.getMonth() + amount); + return d.getTime(); + } + if (baseId === "year" && Number.isInteger(amount)) { + d.setFullYear(d.getFullYear() + amount); + return d.getTime(); + } + return ms; // caller falls back to ms math for everything else +} + +export function isCalendarUnit(unitId: string, amount: number): boolean { + const baseId = unitId.includes(":") ? unitId.split(":")[1] : unitId; + return (baseId === "month" || baseId === "year") && Number.isInteger(amount); +} diff --git a/src/engine/edge.test.ts b/src/engine/edge.test.ts new file mode 100644 index 0000000..2aa34e0 --- /dev/null +++ b/src/engine/edge.test.ts @@ -0,0 +1,144 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { SumEngine } from "./index"; + +let eng: SumEngine; + +beforeAll(() => { + eng = new SumEngine(); + eng.setRates({ USD: 1, EUR: 0.5, RUB: 100, GBP: 0.25 }); +}); + +function calc(expr: string): string | null { + const r = eng.evaluateDocument(expr); + return r[r.length - 1].text; +} + +describe("edge cases", () => { + it("russian mixed sentence", () => { + expect(calc("потратил 20$ и ещё 5$")).toBe("$25"); + }); + it("russian conversion to currency", () => { + expect(calc("20$ в рублях")).toBe("2,000 ₽"); + }); + it("generic 'доллар' means USD, not TTD", () => { + expect(calc("1 доллар")).toBe("$1"); + expect(calc("1 доллар в рублях")).toBe("100 ₽"); + }); + it("chained percent discounts", () => { + expect(calc("100 - 10% - 10%")).toBe("81"); + }); + it("conversion chain", () => { + expect(calc("1 km in m in cm")).toBe("100,000 cm"); + }); + it("euro symbol", () => { + expect(calc("€10 + €5")).toBe("€ 15"); + }); + it("pow precedence", () => { + expect(calc("2 * 3^2")).toBe("18"); + expect(calc("-2^2")).toBe("-4"); + }); + it("last expression wins for unrelated numbers", () => { + expect(calc("page 10 results 2 + 3")).toBe("5"); + }); + it("empty and label lines give no result", () => { + expect(calc("")).toBe(null); + expect(calc("hello world")).toBe(null); + }); + it("sum stops at header", () => { + const r = eng.evaluateDocument("5\n# section\n10\n20\nsum").map((x) => x.text); + expect(r).toEqual(["5", null, "10", "20", "30"]); + }); + it("variables with units survive math", () => { + const r = eng.evaluateDocument("rent = $500\nfood = $200\nrent + food").map((x) => x.text); + expect(r).toEqual(["$500", "$200", "$700"]); + }); + it("big numbers with grouping", () => { + expect(calc("1000000 * 2")).toBe("2,000,000"); + }); + it("division by zero yields no result", () => { + expect(calc("5 / 0")).toBe(null); + }); + it("trailing garbage does not break expression", () => { + expect(calc("2 + 2 =")).toBe("4"); + }); + it("scientific output", () => { + expect(calc("123456 in scientific")).toBe("1.23e5"); // mantissa follows precision setting + }); + it("GiB in GB", () => { + expect(calc("1 GiB in GB")).toBe("1.07 GB"); + }); + it("Kelvin word", () => { + expect(calc("300 kelvin in celsius")).toBe("26.85°C"); + }); + it("milliseconds", () => { + expect(calc("1500 ms in seconds")).toBe("1.5 s"); + }); + it("square meters word form", () => { + expect(calc("3 square meters in sq ft")).toBe("32.29 ft²"); + }); + it("multiplication of money by count", () => { + expect(calc("$12 * 3")).toBe("$36"); + }); +}); + +describe("extra units (beyond original Numi)", () => { + it("speed", () => { + expect(calc("100 km/h in mph")).toBe("62.14 mph"); + expect(calc("60 mph in km/h")).toBe("96.56 km/h"); + expect(calc("10 m/s in km/h")).toBe("36 km/h"); + }); + it("pressure", () => { + expect(calc("1 atm in mmHg")).toBe("760 mmHg"); + expect(calc("2 bar in psi")).toBe("29.01 psi"); + }); + it("energy", () => { + expect(calc("1 kwh in kJ")).toBe("3,600 kJ"); + expect(calc("500 kcal in kJ")).toBe("2,092 kJ"); + }); + it("power", () => { + expect(calc("150 hp in kW")).toBe("111.85 kW"); + expect(calc("100 лс в kW")).toBe("73.55 kW"); + }); + it("frequency", () => { + expect(calc("3000 rpm in Hz")).toBe("50 Hz"); + }); + it("fuel (reciprocal mpg)", () => { + expect(calc("8 l/100km in mpg")).toBe("29.4 mpg"); + expect(calc("30 mpg in l/100km")).toBe("7.84 L/100km"); + }); +}); + +describe("bitwise and unix", () => { + it("bit ops", () => { + expect(calc("0xF0 | 0x0F")).toBe("0xFF"); + expect(calc("0xFF & 0x0F")).toBe("0xF"); + expect(calc("5 xor 3")).toBe("6"); + expect(calc("1 << 4")).toBe("16"); + expect(calc("1 shl 4")).toBe("16"); + expect(calc("256 >> 4")).toBe("16"); + }); + it("unix timestamps", () => { + expect(calc("today in unix")).toMatch(/^\d{10}$/); + const r = eng.evaluateDocument("1750000000 as date"); + expect(r[0].value?.kind).toBe("date"); + }); +}); + +describe("fraction and roman", () => { + it("fractions", () => { + expect(calc("0.75 in fraction")).toBe("3/4"); + expect(calc("1.5 in fraction")).toBe("1 1/2"); + expect(calc("1/3 + 1/3 in fraction")).toBe("2/3"); + }); + it("roman", () => { + expect(calc("2026 in roman")).toBe("MMXXVI"); + expect(calc("49 in roman")).toBe("XLIX"); + }); +}); + +describe("crypto", () => { + it("eth via snapshot/test rates", () => { + const r = calc("1 eth in USD"); + expect(r).toMatch(/^\$/); + }); +}); diff --git a/src/engine/engine.test.ts b/src/engine/engine.test.ts new file mode 100644 index 0000000..5da94d8 --- /dev/null +++ b/src/engine/engine.test.ts @@ -0,0 +1,241 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { SumEngine } from "./index"; + +let eng: SumEngine; + +beforeAll(() => { + eng = new SumEngine(); + // deterministic rates for tests + eng.setRates({ USD: 1, EUR: 0.5, RUB: 100, GBP: 0.25, BTC: 0.00001 }); +}); + +function calc(expr: string): string | null { + const r = eng.evaluateDocument(expr); + return r[r.length - 1].text; +} + +function calcDoc(doc: string): (string | null)[] { + return eng.evaluateDocument(doc).map((r) => r.text); +} + +describe("arithmetic", () => { + it("basic ops", () => { + expect(calc("2 + 2")).toBe("4"); + expect(calc("7 - 10")).toBe("-3"); + expect(calc("6 * 7")).toBe("42"); + expect(calc("1 / 8")).toBe("0.13"); + expect(calc("2 ^ 10")).toBe("1,024"); + expect(calc("10 mod 3")).toBe("1"); + expect(calc("5!")).toBe("120"); + }); + it("decimal precision (no float artifacts)", () => { + expect(calc("0.1 + 0.2")).toBe("0.3"); + }); + it("parens and precedence", () => { + expect(calc("2 + 3 * 4")).toBe("14"); + expect(calc("(2 + 3) * 4")).toBe("20"); + expect(calc("2(3+4)")).toBe("14"); + }); + it("word operators", () => { + expect(calc("5 plus 3")).toBe("8"); + expect(calc("10 minus 4")).toBe("6"); + expect(calc("3 times 4")).toBe("12"); + expect(calc("20 divided by 4")).toBe("5"); + expect(calc("5 и 3")).toBe("8"); // ru plus + }); + it("thousands separators in input", () => { + expect(calc("1,000 + 234")).toBe("1,234"); + }); + it("scales", () => { + expect(calc("1.5 million")).toBe("1,500,000"); + expect(calc("5k")).toBe("5,000"); + expect(calc("2 dozen")).toBe("2"); // dozen is not in Numi vocab — noise + }); + it("free text around math is ignored", () => { + expect(calc("I spent 20 on pizza plus 5 on coffee")).toBe("25"); + }); + it("constants", () => { + expect(calc("pi * 2")).toBe("6.28"); + }); + it("functions", () => { + expect(calc("sqrt(16)")).toBe("4"); + expect(calc("sqrt 16")).toBe("4"); + expect(calc("square root of 16")).toBe("4"); + expect(calc("cbrt(27)")).toBe("3"); + expect(calc("abs(-5)")).toBe("5"); + expect(calc("round(2.6)")).toBe("3"); + expect(calc("log(1000)")).toBe("3"); + expect(calc("ln(1)")).toBe("0"); + }); +}); + +describe("percent", () => { + it("sample: 8 / (45 - 20%)", () => { + expect(calc("8 / (45 - 20%)")).toBe("0.22"); + }); + it("plus/minus percent", () => { + expect(calc("100 + 20%")).toBe("120"); + expect(calc("100 - 20%")).toBe("80"); + }); + it("of / off / on", () => { + expect(calc("20% of 100")).toBe("20"); + expect(calc("20% off 100")).toBe("80"); + expect(calc("20% on 100")).toBe("120"); + }); + it("as a % of", () => { + expect(calc("5 as a % of 25")).toBe("20%"); + }); + it("of what is", () => { + expect(calc("20% of what is 30")).toBe("150"); + expect(calc("25% off what is 60")).toBe("80"); + }); + it("percent arithmetic", () => { + expect(calc("5% + 5%")).toBe("10%"); + expect(calc("100 * 20%")).toBe("20"); + }); +}); + +describe("units", () => { + it("sample: 5 inches in cm", () => { + expect(calc("5 inches in cm")).toBe("12.7 cm"); + }); + it("length", () => { + expect(calc("1 mile in km")).toBe("1.61 km"); + expect(calc("100 km in miles")).toBe("62.14 mi."); + expect(calc("6 feet in meters")).toBe("1.83 m"); + }); + it("prefixed symbols", () => { + expect(calc("10 mm in cm")).toBe("1 cm"); + expect(calc("1 km in m")).toBe("1,000 m"); + }); + it("weight", () => { + expect(calc("1 kg in pounds")).toBe("2.2 lb"); + expect(calc("16 oz in pounds")).toBe("1 lb"); + }); + it("temperature", () => { + expect(calc("100 C in F")).toBe("212°F"); + expect(calc("32 F in C")).toBe("0°C"); + expect(calc("0 C in K")).toBe("273.15 K"); + }); + it("volume", () => { + expect(calc("1 gallon in liters")).toBe("3.79 L"); + }); + it("area", () => { + expect(calc("1 hectare in acres")).toBe("2.47 acre"); + expect(calc("2 m * 3 m")).toBe("6 m²"); + }); + it("data with SI and IEC prefixes", () => { + expect(calc("1 GB in MB")).toBe("1,000 MB"); + expect(calc("1 GiB in MiB")).toBe("1,024 MiB"); + expect(calc("1 GB in MiB")).toBe("953.67 MiB"); + expect(calc("1 byte in bits")).toBe("8 b"); + }); + it("css", () => { + expect(calc("32 px in em")).toBe("2 em"); + expect(calc("1 inch in px")).toBe("96 px"); + }); + it("time units", () => { + expect(calc("90 minutes in hours")).toBe("1.5 h"); + expect(calc("2 hours 30 minutes in minutes")).toBe("150 min"); + }); + it("unit math keeps the left unit", () => { + expect(calc("1 m + 50 cm")).toBe("1.5 m"); + expect(calc("5 ft 4 in in cm")).toBe("162.56 cm"); + }); + it("angle trig", () => { + expect(calc("sin(90 degrees)")).toBe("1"); + }); + it("russian unit phrases", () => { + expect(calc("5 метров в см")).toBe("500 cm"); + }); +}); + +describe("currency", () => { + it("sample: $9 in Euro (test rate 1 USD = 0.5 EUR)", () => { + expect(calc("$9 in Euro")).toBe("€ 4.5"); + }); + it("postfix and codes", () => { + expect(calc("100 USD in RUB")).toBe("10,000 ₽"); + expect(calc("100 рублей в долларах")).toBe("$1"); + }); + it("currency arithmetic", () => { + expect(calc("$10 + $5")).toBe("$15"); + expect(calc("$10 + 20%")).toBe("$12"); + }); +}); + +describe("numeral systems", () => { + it("input", () => { + expect(calc("0x1F")).toBe("0x1F"); + expect(calc("0b101 in decimal")).toBe("5"); + }); + it("conversion", () => { + expect(calc("255 in hex")).toBe("0xFF"); + expect(calc("5 in binary")).toBe("0b101"); + expect(calc("8 in octal")).toBe("0o10"); + }); + it("hex arithmetic keeps repr of the left operand", () => { + expect(calc("0x10 + 0x10 in decimal")).toBe("32"); + }); +}); + +describe("variables and document context", () => { + it("assignment and reuse", () => { + expect(calcDoc("x = 5\nx * 2")).toEqual(["5", "10"]); + }); + it("is-assignment", () => { + expect(calcDoc("price is 100\nprice + 20%")).toEqual(["100", "120"]); + }); + it("sum / total", () => { + expect(calcDoc("10\n20\n30\nsum")).toEqual(["10", "20", "30", "60"]); + expect(calcDoc("10\n20\n\n30\nsum")).toEqual(["10", "20", null, "30", "30"]); + }); + it("sum with units", () => { + expect(calcDoc("$10\n$20\ntotal")).toEqual(["$10", "$20", "$30"]); + }); + it("avg", () => { + expect(calcDoc("10\n20\naverage")).toEqual(["10", "20", "15"]); + }); + it("prev", () => { + expect(calcDoc("42\nprev + 1")).toEqual(["42", "43"]); + }); + it("headers split blocks and produce no result", () => { + expect(calcDoc("# Food\n10\n20\nsum")).toEqual([null, "10", "20", "30"]); + }); + it("comments", () => { + expect(calcDoc("// just a comment\n5 + 5 // inline")).toEqual([null, "10"]); + }); + it("label lines produce no result", () => { + expect(calc("Monthly costs")).toBe(null); + }); +}); + +describe("dates", () => { + it("today + 2 weeks lands 14 days ahead", () => { + const r = eng.evaluateDocument("today + 2 weeks - today"); + // (today + 2w) - today = 14 days + expect(r[0].text).toBe("14 day"); + }); + it("tomorrow - today = 1 day", () => { + expect(calc("tomorrow - today")).toBe("1 day"); + }); + it("time in a timezone returns a formatted time", () => { + const t = calc("time in Tokyo"); + expect(t).toMatch(/^\d{2}:\d{2}$/); + }); +}); + +describe("extension API", () => { + it("addUnit (horse from Sample.js)", () => { + eng.addUnit({ id: "horse", phrases: "horse, horses, hrs", baseUnitId: "meter", format: "hrs", ratio: 2.4 }); + expect(calc("2 horses in meters")).toBe("4.8 m"); + }); + it("addFunction (zum from Sample.js)", () => { + eng.addFunction({ id: "zum", phrases: "zum" }, (values) => ({ double: values[0].double + values[1].double })); + expect(calc("zum(2;3)")).toBe("5"); + }); + it("setVariable", () => { + eng.setVariable("myvar", { double: 5, unitId: "USD" }); + expect(calc("myvar * 2")).toBe("$10"); + }); +}); diff --git a/src/engine/evaluator.ts b/src/engine/evaluator.ts new file mode 100644 index 0000000..eaca6e6 --- /dev/null +++ b/src/engine/evaluator.ts @@ -0,0 +1,466 @@ +// Walks the AST with the document context (variables, line results for +// sum/avg/prev) and produces unit-aware values. +import { Decimal, EvalError, Quantity, Unit, Value, qty, pct } from "./types"; +import { Node, ConvTarget } from "./parser"; +import { Registry } from "./registry"; +import { resolveZone, startOfToday, addToDate, isCalendarUnit } from "./datetime"; + +export interface LineCtx { + /** values of lines above (null = no result) */ + lineValues: (Value | null)[]; + /** per-line flags for block boundaries */ + lineKinds: ("empty" | "header" | "normal")[]; + index: number; +} + +export interface EvalCtx { + reg: Registry; + vars: Map; + line: LineCtx; +} + +const PI = new Decimal("3.14159265358979323846264338327950288419716939937510"); +const E = new Decimal("2.71828182845904523536028747135266249775724709369995"); + +export function evaluate(node: Node, ctx: EvalCtx): Value { + switch (node.k) { + case "num": + return qty(node.v, null, node.repr); + case "const": + return qty(node.name === "pi" ? PI : E); + case "var": { + const v = ctx.vars.get(node.name); + if (!v) throw new EvalError(`unknown variable ${node.name}`); + return v; + } + case "date": + return evalDateWord(node.word); + case "agg": + return evalAgg(node.name, ctx); + case "neg": { + const v = evaluate(node.x, ctx); + if (v.kind === "quantity") return { ...v, value: v.value.neg() }; + if (v.kind === "percent") return pct(v.value.neg()); + throw new EvalError("cannot negate a date"); + } + case "pct": { + const v = evaluate(node.x, ctx); + if (v.kind === "quantity" && !v.unit) return pct(v.value); + if (v.kind === "percent") return v; + throw new EvalError("% needs a plain number"); + } + case "fact": { + const v = asScalar(evaluate(node.x, ctx)); + return qty(factorial(v)); + } + case "unit": { + const v = evaluate(node.x, ctx); + return attachUnit(v, node.unit); + } + case "curr": { + const unit = ctx.reg.makeCurrencyUnit(node.code); + if (!unit) throw new EvalError(`no rate for ${node.code}`); + const v = evaluate(node.x, ctx); + return attachUnit(v, unit); + } + case "scale": { + const v = evaluate(node.x, ctx); + if (v.kind === "quantity") return { ...v, value: v.value.mul(node.mult) }; + if (v.kind === "percent") return pct(v.value.mul(node.mult)); + throw new EvalError("cannot scale a date"); + } + case "bin": + return evalBin(node.op, evaluate(node.l, ctx), evaluate(node.r, ctx), ctx); + case "bit": + return evalBit(node.op, evaluate(node.l, ctx), evaluate(node.r, ctx)); + case "pctop": + return evalPctOp(node.op, evaluate(node.l, ctx), evaluate(node.r, ctx)); + case "conv": + return evalConv(evaluate(node.x, ctx), node.target, ctx); + case "call": + return evalCall(node.name, node.args.map((a) => evaluate(a, ctx)), ctx); + case "seq": { + let acc: Value | null = null; + for (const item of node.items) { + let v: Value; + try { + v = evaluate(item, ctx); + } catch { + continue; + } + if ( + acc?.kind === "quantity" && v.kind === "quantity" && + acc.unit && v.unit && acc.unit.dimension === v.unit.dimension && + acc.unit.dimension !== "scalar" + ) { + // "2 hours 30 min", "5 ft 4 in" — adjacent same-dimension quantities add up + acc = addQ(acc, v); + } else { + acc = v; + } + } + if (!acc) throw new EvalError("empty expression"); + return acc; + } + } +} + +// ---------- helpers + +function asScalar(v: Value): Decimal { + if (v.kind === "quantity") return v.value; + if (v.kind === "percent") return v.value.div(100); + throw new EvalError("expected a number"); +} + +function attachUnit(v: Value, unit: Unit): Value { + if (v.kind === "quantity") { + if (v.unit && v.unit.dimension === unit.dimension) return convertQ(v, unit); + if (v.unit) throw new EvalError("unit mismatch"); + return { ...v, unit, repr: "decimal" }; + } + throw new EvalError("cannot attach unit"); +} + +/** value of q in dimension-base units */ +export function toBase(q: Quantity): Decimal { + if (!q.unit) return q.value; + if (q.unit.reciprocal) { + if (q.value.isZero()) throw new EvalError("division by zero"); + return q.unit.ratio.div(q.value); // mpg -> L/100km + } + let b = q.value.mul(q.unit.ratio); + if (q.unit.offset) b = b.add(q.unit.offset); + return b; +} + +export function fromBase(base: Decimal, unit: Unit): Decimal { + if (unit.reciprocal) { + if (base.isZero()) throw new EvalError("division by zero"); + return unit.ratio.div(base); + } + let v = base; + if (unit.offset) v = v.sub(unit.offset); + return v.div(unit.ratio); +} + +export function convertQ(q: Quantity, unit: Unit): Quantity { + if (!q.unit) return { ...q, unit }; + if (q.unit.dimension !== unit.dimension) throw new EvalError("dimension mismatch"); + return qty(fromBase(toBase(q), unit), unit); +} + +function addQ(a: Quantity, b: Quantity): Quantity { + return numericAdd(a, b, 1); +} + +function numericAdd(a: Quantity, b: Quantity, sign: 1 | -1): Quantity { + if (a.unit && b.unit) { + if (a.unit.dimension !== b.unit.dimension) throw new EvalError("dimension mismatch"); + if (a.unit.offset || b.unit.offset) { + // temperatures: operate in the left unit's scale + const bInA = convertQ(b, a.unit); + return qty(sign === 1 ? a.value.add(bInA.value) : a.value.sub(bInA.value), a.unit); + } + const base = sign === 1 ? toBase(a).add(toBase(b)) : toBase(a).sub(toBase(b)); + return qty(fromBase(base, a.unit), a.unit); + } + const unit = a.unit ?? b.unit ?? null; + return qty(sign === 1 ? a.value.add(b.value) : a.value.sub(b.value), unit); +} + +function evalBin(op: "plus" | "minus" | "mul" | "div" | "mod" | "pow", l: Value, r: Value, ctx: EvalCtx): Value { + // ---- dates + if (l.kind === "date" || r.kind === "date") return evalDateArith(op, l, r); + + // ---- percent combinations + if (l.kind === "percent" || r.kind === "percent") { + if (l.kind === "percent" && r.kind === "percent") { + switch (op) { + case "plus": return pct(l.value.add(r.value)); + case "minus": return pct(l.value.sub(r.value)); + case "mul": return pct(l.value.mul(r.value).div(100)); + case "div": return qty(l.value.div(r.value)); + default: throw new EvalError("bad percent op"); + } + } + const q = (l.kind === "quantity" ? l : r) as Quantity; + const p = (l.kind === "percent" ? l : r).value; + switch (op) { + case "plus": return qty(q.value.mul(new Decimal(1).add(p.div(100))), q.unit); + case "minus": + if (l.kind === "percent") throw new EvalError("percent minus number"); + return qty(q.value.mul(new Decimal(1).sub(p.div(100))), q.unit); + case "mul": return qty(q.value.mul(p).div(100), q.unit); + case "div": + if (l.kind === "percent") throw new EvalError("percent div number"); + return qty(q.value.div(p.div(100)), q.unit); + case "pow": return qty(q.value.pow(p.div(100)), q.unit); + default: throw new EvalError("bad percent op"); + } + } + + const a = l as Quantity; + const b = r as Quantity; + switch (op) { + case "plus": return numericAdd(a, b, 1); + case "minus": return numericAdd(a, b, -1); + case "mul": { + if (a.unit && b.unit) { + const da = a.unit.dimension; + const db = b.unit.dimension; + const baseProduct = toBase(a).mul(toBase(b)); + let targetId: string | null = null; + if (da === "length" && db === "length") targetId = `sq:${a.unit.id}`; + else if (da === "length" && db === "area") targetId = `cb:${a.unit.id}`; + else if (da === "area" && db === "length") targetId = `cb:${b.unit.id}`; + if (targetId) { + const u = ctx.reg.unitsById.get(targetId); + if (u) return qty(baseProduct.div(u.ratio), u); + } + return qty(a.value.mul(b.value), a.unit); + } + return qty(a.value.mul(b.value), a.unit ?? b.unit ?? null); + } + case "div": { + if (b.value.isZero()) throw new EvalError("division by zero"); + if (a.unit && b.unit && a.unit.dimension === b.unit.dimension) { + return qty(toBase(a).div(toBase(b))); + } + return qty(a.value.div(b.value), a.unit ?? null); + } + case "mod": { + if (b.value.isZero()) throw new EvalError("mod zero"); + return qty(a.value.mod(b.value), a.unit ?? null); + } + case "pow": { + const result = a.value.pow(b.value); + if (a.unit?.dimension === "length" && !b.unit) { + const id = b.value.eq(2) ? `sq:${a.unit.id}` : b.value.eq(3) ? `cb:${a.unit.id}` : null; + const u = id ? ctx.reg.unitsById.get(id) : null; + if (u) return qty(result, u); + } + return qty(result, b.unit ? null : a.unit && b.value.eq(1) ? a.unit : null); + } + } +} + +function evalBit(op: string, l: Value, r: Value): Value { + if (l.kind !== "quantity" || r.kind !== "quantity") throw new EvalError("bitwise needs integers"); + const a = BigInt(l.value.toDecimalPlaces(0).toFixed(0)); + const b = BigInt(r.value.toDecimalPlaces(0).toFixed(0)); + if ((op === "shl" || op === "shr") && (b < 0n || b > 256n)) throw new EvalError("bad shift"); + let out: bigint; + switch (op) { + case "band": out = a & b; break; + case "bor": out = a | b; break; + case "bxor": out = a ^ b; break; + case "shl": out = a << b; break; + case "shr": out = a >> b; break; + default: throw new EvalError("bad bit op"); + } + const repr = l.repr !== "decimal" ? l.repr : r.repr; + return qty(new Decimal(out.toString()), null, repr); +} + +function evalPctOp(op: string, l: Value, r: Value): Value { + const rq = r.kind === "quantity" ? r : null; + const rv = r.kind === "percent" ? r.value.div(100) : rq ? rq.value : null; + if (rv === null) throw new EvalError("bad operand"); + const unit = rq?.unit ?? null; + + if (op === "of" || op === "off" || op === "on") { + const p = l.kind === "percent" ? l.value : asScalar(l).mul(100); + switch (op) { + case "of": return qty(rv.mul(p).div(100), unit); + case "off": return qty(rv.mul(new Decimal(1).sub(p.div(100))), unit); + case "on": return qty(rv.mul(new Decimal(1).add(p.div(100))), unit); + } + } + if (op === "as_pct_of" || op === "as_pct_off" || op === "as_pct_on") { + const lv = l.kind === "quantity" ? l.value : asScalar(l); + if (rv.isZero()) throw new EvalError("division by zero"); + switch (op) { + case "as_pct_of": return pct(lv.div(rv).mul(100)); + case "as_pct_off": return pct(new Decimal(1).sub(lv.div(rv)).mul(100)); + case "as_pct_on": return pct(lv.div(rv).sub(1).mul(100)); + } + } + // "20% of what is 30" + const p = l.kind === "percent" ? l.value : asScalar(l).mul(100); + switch (op) { + case "of_what_is": return qty(rv.div(p.div(100)), unit); + case "off_what_is": { + const d = new Decimal(1).sub(p.div(100)); + if (d.isZero()) throw new EvalError("division by zero"); + return qty(rv.div(d), unit); + } + case "on_what_is": return qty(rv.div(new Decimal(1).add(p.div(100))), unit); + } + throw new EvalError(`bad percent op ${op}`); +} + +function evalConv(v: Value, target: ConvTarget, ctx: EvalCtx): Value { + switch (target.type) { + case "unit": { + if (v.kind === "percent") return pct(v.value); // "x in %" — keep + if (v.kind !== "quantity") throw new EvalError("cannot convert date to unit"); + if (!v.unit) return qty(v.value, target.unit); + return convertQ(v, target.unit); + } + case "currency": { + const unit = ctx.reg.makeCurrencyUnit(target.code); + if (!unit) throw new EvalError(`no rate for ${target.code}`); + if (v.kind !== "quantity") throw new EvalError("cannot convert to currency"); + if (!v.unit) return qty(v.value, unit); + return convertQ(v, unit); + } + case "repr": { + if (v.kind !== "quantity") throw new EvalError("bad repr conversion"); + return { ...v, repr: target.repr }; + } + case "scale": { + if (v.kind !== "quantity") throw new EvalError("bad scale conversion"); + return qty(v.value.div(target.mult), v.unit); // formatter shows plain number + } + case "tz": { + const zone = resolveZone(target.words); + if (!zone) throw new EvalError("unknown timezone"); + if (v.kind !== "date") throw new EvalError("timezone needs a time"); + return { ...v, timeZone: zone }; + } + case "unix": { + if (v.kind === "date") return qty(new Decimal(v.ms).div(1000).floor(), null, "plain"); + throw new EvalError("unix needs a date"); + } + case "todate": { + if (v.kind !== "quantity" || v.unit) throw new EvalError("date needs a plain number"); + // heuristics: > 1e12 means milliseconds, else seconds + const ms = v.value.gt(1e12) ? v.value.toNumber() : v.value.mul(1000).toNumber(); + return { kind: "date", ms, hasTime: true }; + } + } +} + +function evalDateWord(word: string): Value { + switch (word) { + case "today": return { kind: "date", ms: startOfToday(), hasTime: false }; + case "tomorrow": return { kind: "date", ms: startOfToday() + 86400_000, hasTime: false }; + case "yesterday": return { kind: "date", ms: startOfToday() - 86400_000, hasTime: false }; + case "now": return { kind: "date", ms: Date.now(), hasTime: true }; + case "time": case "local": return { kind: "date", ms: Date.now(), hasTime: true, timeOnly: true }; + default: throw new EvalError(`bad date word ${word}`); + } +} + +function evalDateArith(op: string, l: Value, r: Value): Value { + if (l.kind === "date" && r.kind === "date") { + if (op !== "minus") throw new EvalError("dates only subtract"); + const days = new Decimal(l.ms - r.ms).div(86400_000); + return qty(days, { id: "day", dimension: "time", ratio: new Decimal(86400), format: "day" }); + } + const date = (l.kind === "date" ? l : r) as Extract; + const span = l.kind === "date" ? r : l; + if (span.kind !== "quantity" || span.unit?.dimension !== "time") { + throw new EvalError("date arithmetic needs a time span"); + } + if (op !== "plus" && op !== "minus") throw new EvalError("bad date op"); + if (op === "minus" && l.kind !== "date") throw new EvalError("cannot subtract date from number"); + const amount = span.value.toNumber() * (op === "minus" ? -1 : 1); + let ms: number; + if (isCalendarUnit(span.unit.id, amount)) { + ms = addToDate(date.ms, amount, span.unit.id); + } else { + ms = date.ms + span.value.mul(span.unit.ratio).mul(1000).toNumber() * (op === "minus" ? -1 : 1); + } + return { ...date, ms }; +} + +function evalAgg(name: "sum" | "avg" | "prev", ctx: EvalCtx): Value { + const { lineValues, lineKinds, index } = ctx.line; + if (name === "prev") { + for (let i = index - 1; i >= 0; i--) { + const v = lineValues[i]; + if (v) return v; + } + throw new EvalError("no previous result"); + } + const block: Quantity[] = []; + for (let i = index - 1; i >= 0; i--) { + if (lineKinds[i] === "empty" || lineKinds[i] === "header") { + if (block.length > 0) break; + if (lineKinds[i] === "header") break; + continue; // skip empties directly above until the block starts + } + const v = lineValues[i]; + if (v?.kind === "quantity") block.push(v); + } + if (block.length === 0) throw new EvalError("nothing to aggregate"); + block.reverse(); + let acc = block[0]; + for (let i = 1; i < block.length; i++) { + const item = block[i]; + try { + acc = numericAdd(acc, item, 1); + } catch { + // skip lines with incompatible dimensions, as Numi does + } + } + if (name === "avg") return qty(acc.value.div(block.length), acc.unit); + return acc; +} + +function evalCall(name: string, args: Value[], ctx: EvalCtx): Value { + const custom = ctx.reg.customFuncs.get(name); + if (custom) return custom(args); + + const x = args[0]; + if (!x) throw new EvalError(`${name} needs an argument`); + + // trig works on angles: degrees convert to radians + const angleArg = (): Decimal => { + if (x.kind === "quantity" && x.unit?.dimension === "angle") { + return x.value.mul(x.unit.ratio); + } + return asScalar(x); + }; + const n = () => asScalar(x); + + switch (name) { + case "sqrt": { + if (x.kind === "quantity" && x.unit?.dimension === "area" && x.unit.id.startsWith("sq:")) { + const base = ctx.reg.unitsById.get(x.unit.id.slice(3)); + if (base) return qty(x.value.sqrt(), base); + } + return qty(n().sqrt()); + } + case "cbrt": return qty(n().cbrt()); + case "sin": return qty(Decimal.sin(angleArg())); + case "cos": return qty(Decimal.cos(angleArg())); + case "tan": return qty(Decimal.tan(angleArg())); + case "cot": return qty(new Decimal(1).div(Decimal.tan(angleArg()))); + case "asin": case "arcsin": return qty(Decimal.asin(n())); + case "acos": case "arccos": return qty(Decimal.acos(n())); + case "atan": case "arctan": return qty(Decimal.atan(n())); + case "sinh": return qty(Decimal.sinh(n())); + case "cosh": return qty(Decimal.cosh(n())); + case "tanh": return qty(Decimal.tanh(n())); + case "ln": return qty(Decimal.ln(n())); + case "lg": case "log": return qty(Decimal.log10(n())); + case "log2": return qty(Decimal.log2(n())); + case "exp": return qty(Decimal.exp(n())); + case "abs": return x.kind === "quantity" ? { ...x, value: x.value.abs() } : qty(n().abs()); + case "round": return x.kind === "quantity" ? { ...x, value: x.value.round() } : qty(n().round()); + case "ceil": return x.kind === "quantity" ? { ...x, value: x.value.ceil() } : qty(n().ceil()); + case "floor": return x.kind === "quantity" ? { ...x, value: x.value.floor() } : qty(n().floor()); + case "fact": case "factorial": return qty(factorial(n())); + default: throw new EvalError(`unknown function ${name}`); + } +} + +function factorial(n: Decimal): Decimal { + if (!n.isInteger() || n.isNegative() || n.gt(300)) throw new EvalError("bad factorial"); + let acc = new Decimal(1); + for (let i = 2; i <= n.toNumber(); i++) acc = acc.mul(i); + return acc; +} diff --git a/src/engine/extraunits.ts b/src/engine/extraunits.ts new file mode 100644 index 0000000..18787ae --- /dev/null +++ b/src/engine/extraunits.ts @@ -0,0 +1,98 @@ +// Speed, pressure, energy, power, frequency and fuel consumption units, +// plus cryptocurrencies. Phrases inline (en + ru). + +export interface ExtraUnitDef { + id: string; + dimension: "speed" | "pressure" | "energy" | "power" | "frequency" | "fuel"; + /** multiplier to the dimension base: m/s, Pa, J, W, Hz, L/100km */ + ratio: string; + /** reciprocal unit: base = ratio / value (mpg) */ + reciprocal?: boolean; + format: string; + /** comma-separated, matched case-insensitively */ + phrases: string; + /** matched case-sensitively (symbols like kW vs KW ambiguity) */ + symbols?: string; +} + +export const EXTRA_UNITS: ExtraUnitDef[] = [ + // ---- speed (base: m/s) + { id: "mps", dimension: "speed", ratio: "1", format: "m/s", phrases: "m/s,meter per second,meters per second,м/с,метров в секунду" }, + { id: "kmh", dimension: "speed", ratio: String(1 / 3.6), format: "km/h", phrases: "km/h,kmh,kph,kilometers per hour,kilometres per hour,км/ч,кмч,километров в час" }, + { id: "mph", dimension: "speed", ratio: "0.44704", format: "mph", phrases: "mph,miles per hour,миль в час" }, + { id: "knot", dimension: "speed", ratio: "0.514444444", format: "kn", phrases: "knot,knots,kn,узел,узла,узлов" }, + { id: "fps", dimension: "speed", ratio: "0.3048", format: "ft/s", phrases: "ft/s,feet per second,фут/с" }, + + // ---- pressure (base: Pa) + { id: "pascal", dimension: "pressure", ratio: "1", format: "Pa", phrases: "pascal,pascals,паскаль,паскалей", symbols: "Pa" }, + { id: "kpa", dimension: "pressure", ratio: "1000", format: "kPa", phrases: "kilopascal,kilopascals,килопаскаль", symbols: "kPa" }, + { id: "mpa", dimension: "pressure", ratio: "1e6", format: "MPa", phrases: "megapascal,megapascals,мегапаскаль", symbols: "MPa" }, + { id: "hpa", dimension: "pressure", ratio: "100", format: "hPa", phrases: "hectopascal,hectopascals,гектопаскаль", symbols: "hPa" }, + { id: "bar", dimension: "pressure", ratio: "1e5", format: "bar", phrases: "bar,bars,бар,бара,баров" }, + { id: "mbar", dimension: "pressure", ratio: "100", format: "mbar", phrases: "mbar,millibar,millibars,миллибар" }, + { id: "atm", dimension: "pressure", ratio: "101325", format: "atm", phrases: "atm,atmosphere,atmospheres,атмосфер,атмосфера,атмосферы" }, + { id: "psi", dimension: "pressure", ratio: "6894.757293168", format: "psi", phrases: "psi" }, + { id: "mmhg", dimension: "pressure", ratio: "133.322387415", format: "mmHg", phrases: "mmhg,mm hg,torr,мм рт ст,мм рт. ст.,миллиметров ртутного столба", symbols: "mmHg" }, + + // ---- energy (base: J) + { id: "joule", dimension: "energy", ratio: "1", format: "J", phrases: "joule,joules,джоуль,джоулей,джоуля", symbols: "J" }, + { id: "kj", dimension: "energy", ratio: "1000", format: "kJ", phrases: "kilojoule,kilojoules,килоджоуль,килоджоулей", symbols: "kJ" }, + { id: "mj", dimension: "energy", ratio: "1e6", format: "MJ", phrases: "megajoule,megajoules,мегаджоуль", symbols: "MJ" }, + { id: "cal", dimension: "energy", ratio: "4.184", format: "cal", phrases: "calorie,calories,калория,калорий,калории,cal" }, + { id: "kcal", dimension: "energy", ratio: "4184", format: "kcal", phrases: "kcal,kilocalorie,kilocalories,ккал,килокалорий,килокалория" }, + { id: "wh", dimension: "energy", ratio: "3600", format: "Wh", phrases: "watt hour,watt hours,ватт-час,ватт-часов", symbols: "Wh" }, + { id: "kwh", dimension: "energy", ratio: "3.6e6", format: "kWh", phrases: "kwh,kilowatt hour,kilowatt hours,киловатт-час,киловатт-часов,квтч,квт-ч", symbols: "kWh" }, + { id: "mwh", dimension: "energy", ratio: "3.6e9", format: "MWh", phrases: "megawatt hour,megawatt hours,мегаватт-час", symbols: "MWh" }, + + // ---- power (base: W) + { id: "watt", dimension: "power", ratio: "1", format: "W", phrases: "watt,watts,ватт,ваттов", symbols: "W" }, + { id: "kw", dimension: "power", ratio: "1000", format: "kW", phrases: "kilowatt,kilowatts,киловатт,киловатта,квт", symbols: "kW" }, + { id: "mw", dimension: "power", ratio: "1e6", format: "MW", phrases: "megawatt,megawatts,мегаватт", symbols: "MW" }, + { id: "gw", dimension: "power", ratio: "1e9", format: "GW", phrases: "gigawatt,gigawatts,гигаватт", symbols: "GW" }, + { id: "hp", dimension: "power", ratio: "745.69987158", format: "hp", phrases: "hp,horsepower,horse power" }, + { id: "hp_metric", dimension: "power", ratio: "735.49875", format: "л.с.", phrases: "л.с.,лс,лошадиных сил,лошадиная сила,лошадиные силы,metric horsepower" }, + + // ---- frequency (base: Hz) + { id: "hz", dimension: "frequency", ratio: "1", format: "Hz", phrases: "hertz,герц,герца", symbols: "Hz" }, + { id: "khz", dimension: "frequency", ratio: "1e3", format: "kHz", phrases: "kilohertz,килогерц", symbols: "kHz" }, + { id: "mhz", dimension: "frequency", ratio: "1e6", format: "MHz", phrases: "megahertz,мегагерц", symbols: "MHz" }, + { id: "ghz", dimension: "frequency", ratio: "1e9", format: "GHz", phrases: "gigahertz,гигагерц", symbols: "GHz" }, + { id: "rpm", dimension: "frequency", ratio: String(1 / 60), format: "rpm", phrases: "rpm,revolutions per minute,оборотов в минуту,об/мин" }, + + // ---- fuel consumption (base: L/100km; mpg is reciprocal) + { id: "l100km", dimension: "fuel", ratio: "1", format: "L/100km", phrases: "l/100km,l/100 km,liters per 100 km,litres per 100 km,л/100км,л/100 км,литров на 100 км,литров на сотню" }, + { id: "mpg", dimension: "fuel", ratio: "235.214583", reciprocal: true, format: "mpg", phrases: "mpg,miles per gallon,миль на галлон" }, + { id: "mpg_imp", dimension: "fuel", ratio: "282.480936", reciprocal: true, format: "mpg (imp)", phrases: "imperial mpg,mpg imperial,uk mpg" }, +]; + +/** Cryptocurrencies beyond BTC. code -> CoinGecko id + phrases + USD snapshot price. */ +export interface CryptoDef { + code: string; + geckoId: string; + phrases: string; + /** approximate USD price for the offline snapshot */ + snapshotUsd: number; +} + +export const CRYPTO: CryptoDef[] = [ + { code: "ETH", geckoId: "ethereum", phrases: "ethereum,ether,eth,эфир,эфириум", snapshotUsd: 2600 }, + { code: "SOL", geckoId: "solana", phrases: "solana,солана", snapshotUsd: 160 }, + { code: "BNB", geckoId: "binancecoin", phrases: "bnb,binance coin", snapshotUsd: 640 }, + { code: "XRP", geckoId: "ripple", phrases: "xrp,ripple,рипл", snapshotUsd: 2.2 }, + { code: "ADA", geckoId: "cardano", phrases: "cardano,ada,кардано", snapshotUsd: 0.7 }, + { code: "DOGE", geckoId: "dogecoin", phrases: "dogecoin,doge,доджкоин,дож", snapshotUsd: 0.18 }, + { code: "TRX", geckoId: "tron", phrases: "tron,trx,трон", snapshotUsd: 0.27 }, + { code: "TON", geckoId: "the-open-network", phrases: "toncoin,тонкоин", snapshotUsd: 3.5 }, + { code: "DOT", geckoId: "polkadot", phrases: "polkadot,полкадот", snapshotUsd: 4.5 }, + { code: "LTC", geckoId: "litecoin", phrases: "litecoin,ltc,лайткоин", snapshotUsd: 90 }, + { code: "AVAX", geckoId: "avalanche-2", phrases: "avalanche,avax", snapshotUsd: 25 }, + { code: "LINK", geckoId: "chainlink", phrases: "chainlink,чейнлинк", snapshotUsd: 15 }, + { code: "UNI", geckoId: "uniswap", phrases: "uniswap,юнисвап", snapshotUsd: 8 }, + { code: "XLM", geckoId: "stellar", phrases: "stellar,xlm,стеллар", snapshotUsd: 0.3 }, + { code: "XMR", geckoId: "monero", phrases: "monero,xmr,монеро", snapshotUsd: 170 }, + { code: "ATOM", geckoId: "cosmos", phrases: "cosmos,космос", snapshotUsd: 5 }, + { code: "BCH", geckoId: "bitcoin-cash", phrases: "bitcoin cash,bch", snapshotUsd: 450 }, + { code: "NEAR", geckoId: "near", phrases: "near protocol", snapshotUsd: 3 }, + { code: "USDT", geckoId: "tether", phrases: "tether,usdt,юсдт", snapshotUsd: 1 }, + { code: "USDC", geckoId: "usd-coin", phrases: "usdc", snapshotUsd: 1 }, +]; diff --git a/src/engine/formatter.ts b/src/engine/formatter.ts new file mode 100644 index 0000000..12a8b1e --- /dev/null +++ b/src/engine/formatter.ts @@ -0,0 +1,116 @@ +// Turns values into display strings: unit/currency formats plus +// precision and grouping settings. +import { Decimal, EngineSettings, Value } from "./types"; + +function group(intPart: string, sep: string): string { + if (!sep) return intPart; + const neg = intPart.startsWith("-"); + const digits = neg ? intPart.slice(1) : intPart; + if (digits.length < 4) return intPart; + const grouped = digits.replace(/\B(?=(\d{3})+(?!\d))/g, sep); + return (neg ? "-" : "") + grouped; +} + +export function formatNumber(v: Decimal, s: EngineSettings): string { + const rounded = v.toDecimalPlaces(s.precision); + const str = rounded.toString(); + if (str.includes("e") || str.includes("E")) return str; + const [int, frac] = str.split("."); + const intG = group(int, s.groupSeparator); + return frac !== undefined ? intG + s.decimalSeparator + frac : intG; +} + +export function formatValue(v: Value, s: EngineSettings): string { + if (v.kind === "percent") { + return formatNumber(v.value, s) + "%"; + } + + if (v.kind === "date") { + const opts: Intl.DateTimeFormatOptions = { timeZone: v.timeZone }; + if (v.timeOnly) { + return new Intl.DateTimeFormat(undefined, { ...opts, hour: "2-digit", minute: "2-digit", hour12: false }).format(v.ms); + } + if (v.hasTime) { + return new Intl.DateTimeFormat(undefined, { + ...opts, hour: "2-digit", minute: "2-digit", hour12: false, + day: "numeric", month: "short", year: "numeric", + }).format(v.ms); + } + return new Intl.DateTimeFormat(undefined, { ...opts, day: "numeric", month: "long", year: "numeric" }).format(v.ms); + } + + // quantity + if (v.repr === "hex" || v.repr === "binary" || v.repr === "octal") { + const int = v.value.toDecimalPlaces(0, Decimal.ROUND_HALF_UP); + const neg = int.isNegative(); + const abs = int.abs(); + const prefix = v.repr === "hex" ? "0x" : v.repr === "binary" ? "0b" : "0o"; + const radix = v.repr === "hex" ? 16 : v.repr === "binary" ? 2 : 8; + const digits = BigInt(abs.toFixed(0)).toString(radix); + return (neg ? "-" : "") + prefix + (v.repr === "hex" ? digits.toUpperCase() : digits); + } + if (v.repr === "scientific") { + return v.value.toExponential(Math.min(s.precision, 10)).replace(/e\+?/, "e"); + } + if (v.repr === "plain") { + return v.value.toString(); // no grouping (unix timestamps) + } + if (v.repr === "fraction") { + return formatFraction(v.value); + } + if (v.repr === "roman") { + return formatRoman(v.value) ?? formatNumber(v.value, s); + } + + const num = formatNumber(v.value, s); + if (!v.unit) return num; + return withUnit(num, v.unit.format); +} + +function withUnit(num: string, f: string): string { + if (f.includes("{}")) { + return f.replace("{}", num); // currency template + } + // suffix units: letters get a space ("12.7 cm"), symbols attach ("5″", "25°C") + return /^[\p{L}]/u.test(f) && !f.startsWith("°") ? `${num} ${f}` : `${num}${f}`; +} + +/** Best rational approximation via continued fractions: 0.75 -> "3/4". */ +function formatFraction(v: Decimal): string { + const neg = v.isNegative(); + const x = v.abs().toNumber(); + if (!Number.isFinite(x)) return v.toString(); + let h1 = 1, h0 = 0, k1 = 0, k0 = 1, b = x; + for (let i = 0; i < 40; i++) { + const a = Math.floor(b); + [h1, h0] = [a * h1 + h0, h1]; + [k1, k0] = [a * k1 + k0, k1]; + if (k1 > 10000) break; + if (Math.abs(x - h1 / k1) < 1e-12) break; + const frac = b - a; + if (frac < 1e-12) break; + b = 1 / frac; + } + const sign = neg ? "-" : ""; + if (k1 === 1) return sign + String(h1); + const whole = Math.floor(h1 / k1); + const rem = h1 - whole * k1; + return whole > 0 ? `${sign}${whole} ${rem}/${k1}` : `${sign}${rem}/${k1}`; +} + +function formatRoman(v: Decimal): string | null { + if (!v.isInteger() || v.lt(1) || v.gt(3999)) return null; + let n = v.toNumber(); + const table: Array<[number, string]> = [ + [1000, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"], + [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"], + ]; + let out = ""; + for (const [val, sym] of table) { + while (n >= val) { + out += sym; + n -= val; + } + } + return out; +} diff --git a/src/engine/index.ts b/src/engine/index.ts new file mode 100644 index 0000000..ea69f41 --- /dev/null +++ b/src/engine/index.ts @@ -0,0 +1,217 @@ +// Public facade: evaluates whole documents line by line and exposes the +// extension API (numi.addUnit / addFunction / setVariable). +import { Decimal, EngineSettings, EvalError, Quantity, Unit, Value, defaultSettings, qty } from "./types"; +import { Registry, buildRegistry, Completion } from "./registry"; +import { tokenize, Token } from "./tokenizer"; +import { parseLine } from "./parser"; +import { evaluate, EvalCtx, toBase, fromBase } from "./evaluator"; +import { formatValue } from "./formatter"; +import snapshot from "./rates-snapshot.json"; +import { CRYPTO } from "./extraunits"; + +const cryptoSnapshot: Record = {}; +for (const c of CRYPTO) cryptoSnapshot[c.code] = 1 / c.snapshotUsd; + +export interface LineResult { + /** formatted result, null when the line has none (text, comment, header, error) */ + text: string | null; + value: Value | null; + kind: "empty" | "header" | "comment" | "normal"; + /** tokens, for syntax highlighting */ + tokens: Token[]; + /** offset of "//" comment start within the line, if any */ + commentStart: number | null; +} + +export interface ExtensionUnitSpec { + id: string; + phrases: string; + baseUnitId: string; + format?: string; + ratio: number; +} + +export interface ExtensionValue { + double: number; + unitId?: string; +} + +export class SumEngine { + reg: Registry; + settings: EngineSettings; + /** variables defined by extensions, persist across evaluations */ + private globals = new Map(); + + constructor(settings: Partial = {}) { + this.settings = { ...defaultSettings, ...settings }; + this.reg = buildRegistry(); + this.reg.setRates({ ...snapshot.rates, ...cryptoSnapshot }); + } + + setRates(rates: Record): void { + // snapshot fills codes the live API doesn't return (e.g. crypto when CoinGecko fails) + this.reg.setRates({ ...snapshot.rates, ...cryptoSnapshot, ...rates }); + } + + updateSettings(patch: Partial): void { + this.settings = { ...this.settings, ...patch }; + } + + completions(): Completion[] { + return this.reg.completions; + } + + evaluateDocument(text: string): LineResult[] { + const lines = text.split("\n"); + const results: LineResult[] = []; + const vars = new Map(this.globals); + const lineValues: (Value | null)[] = []; + const lineKinds: ("empty" | "header" | "normal")[] = []; + + for (let i = 0; i < lines.length; i++) { + const rawLine = lines[i]; + const commentStart = rawLine.indexOf("//"); + const line = commentStart >= 0 ? rawLine.slice(0, commentStart) : rawLine; + const trimmed = line.trim(); + + if (trimmed.length === 0) { + const kind = commentStart >= 0 ? "comment" : "empty"; + results.push({ text: null, value: null, kind, tokens: [], commentStart: commentStart >= 0 ? commentStart : null }); + lineValues.push(null); + lineKinds.push("empty"); + continue; + } + if (trimmed.startsWith("#")) { + results.push({ text: null, value: null, kind: "header", tokens: [], commentStart: commentStart >= 0 ? commentStart : null }); + lineValues.push(null); + lineKinds.push("header"); + continue; + } + + const tokens = tokenize(line, this.reg); + const knownVars = new Set(vars.keys()); + const parsed = parseLine(tokens, knownVars); + + let value: Value | null = null; + if (parsed.expr) { + const ctx: EvalCtx = { + reg: this.reg, + vars, + line: { lineValues, lineKinds, index: i }, + }; + try { + value = evaluate(parsed.expr, ctx); + if (parsed.assign) vars.set(parsed.assign, value); + } catch (e) { + if (!(e instanceof EvalError)) throw e; + value = null; + } + } + + lineValues.push(value); + lineKinds.push("normal"); + results.push({ + text: value ? formatValue(value, this.settings) : null, + value, + kind: "normal", + tokens, + commentStart: commentStart >= 0 ? commentStart : null, + }); + } + return results; + } + + /** Evaluate a single expression (used by the extension runtime and tests). */ + evaluateExpression(text: string): Value | null { + const r = this.evaluateDocument(text); + return r[r.length - 1]?.value ?? null; + } + + /** + * Grand total of the sheet for the status bar: sums every line result + * compatible with the first united result's dimension (converted to its + * unit). Returns null when fewer than two lines contribute. + */ + totalOf(results: LineResult[]): string | null { + const qs = results + .map((r) => r.value) + .filter((v): v is Quantity => v !== null && v.kind === "quantity"); + if (qs.length < 2) return null; + const anchor = qs.find((q) => q.unit) ?? qs[0]; + let acc = new Decimal(0); + let count = 0; + for (const q of qs) { + if (anchor.unit && q.unit && q.unit.dimension === anchor.unit.dimension) { + acc = acc.add(toBase(q)); + count++; + } else if (!anchor.unit && !q.unit) { + acc = acc.add(q.value); + count++; + } + } + if (count < 2) return null; + const value = anchor.unit ? fromBase(acc, anchor.unit) : acc; + return formatValue(qty(value, anchor.unit ?? null), this.settings); + } + + // ---------- extension API (numi.*) + + setVariable(name: string, value: number | ExtensionValue): void { + this.globals.set(name, this.toValue(value)); + } + + addUnit(spec: ExtensionUnitSpec): void { + const base = this.reg.unitsById.get(spec.baseUnitId) ?? this.currencyBase(spec.baseUnitId); + if (!base) throw new Error(`numi.addUnit: unknown baseUnitId ${spec.baseUnitId}`); + const unit: Unit = { + id: spec.id, + dimension: base.dimension, + ratio: base.ratio.mul(spec.ratio), + ...(base.offset ? { offset: base.offset } : {}), + format: spec.format ?? spec.id, + }; + this.reg.unitsById.set(unit.id, unit); + for (const p of spec.phrases.split(",")) { + const phrase = p.trim(); + if (phrase) this.reg.addPhrase(phrase, { t: "unit", unit }, { caseSensitive: false }); + } + this.reg.completions.push({ label: spec.id, type: "unit", detail: "extension" }); + } + + addFunction(spec: { id: string; phrases: string }, fn: (values: ExtensionValue[]) => ExtensionValue | number): void { + const name = spec.id; + this.reg.customFuncs.set(name, (args: Value[]) => { + const mapped = args.map((a) => this.fromValue(a)); + return this.toValue(fn(mapped)); + }); + for (const p of (spec.phrases || spec.id).split(",")) { + const phrase = p.trim(); + if (phrase) this.reg.addPhrase(phrase, { t: "func", name }, { caseSensitive: false }); + } + this.reg.completions.push({ label: name, type: "function", detail: "extension" }); + } + + private currencyBase(code: string): Unit | null { + return this.reg.currencyCodes.has(code) ? this.reg.makeCurrencyUnit(code) : null; + } + + private toValue(v: number | ExtensionValue): Value { + if (typeof v === "number") return qty(v); + const unit = v.unitId + ? this.reg.unitsById.get(v.unitId) ?? this.currencyBase(v.unitId) ?? null + : null; + return qty(new Decimal(v.double), unit); + } + + private fromValue(v: Value): ExtensionValue { + if (v.kind === "quantity") { + return { double: v.value.toNumber(), ...(v.unit ? { unitId: v.unit.id } : {}) }; + } + if (v.kind === "percent") return { double: v.value.div(100).toNumber() }; + return { double: v.ms }; + } +} + +export { formatValue } from "./formatter"; +export type { Value, EngineSettings } from "./types"; +export type { Token } from "./tokenizer"; diff --git a/src/engine/lexer.ts b/src/engine/lexer.ts new file mode 100644 index 0000000..4eed6e9 --- /dev/null +++ b/src/engine/lexer.ts @@ -0,0 +1,79 @@ +// Splits a line into raw lexemes (numbers, words, symbols). Shared by the +// tokenizer and the phrase registry so both segment text the same way. +import { Decimal, NumeralRepr } from "./types"; + +export interface Lex { + type: "num" | "word" | "sym"; + raw: string; + start: number; + end: number; + value?: Decimal; + repr?: NumeralRepr; +} + +const WORD_RE = /^[\p{L}_]+/u; +// Order matters: grouped thousands first, then plain decimal/exponent, then leading-dot. +const NUM_RE = /^(?:\d{1,3}(?:,\d{3})+(?:\.\d+)?|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|\.\d+)/; + +export function lexLine(text: string): Lex[] { + const out: Lex[] = []; + let i = 0; + while (i < text.length) { + const ch = text[i]; + if (ch === " " || ch === "\t" || ch === " ") { + i++; + continue; + } + const rest = text.slice(i); + + if (ch >= "0" && ch <= "9") { + const hex = /^0[xX][0-9a-fA-F]+/.exec(rest); + if (hex) { + out.push({ type: "num", raw: hex[0], start: i, end: i + hex[0].length, value: new Decimal(parseInt(hex[0], 16)), repr: "hex" }); + i += hex[0].length; + continue; + } + const bin = /^0[bB][01]+/.exec(rest); + if (bin) { + out.push({ type: "num", raw: bin[0], start: i, end: i + bin[0].length, value: new Decimal(parseInt(bin[0].slice(2), 2)), repr: "binary" }); + i += bin[0].length; + continue; + } + const oct = /^0[oO][0-7]+/.exec(rest); + if (oct) { + out.push({ type: "num", raw: oct[0], start: i, end: i + oct[0].length, value: new Decimal(parseInt(oct[0].slice(2), 8)), repr: "octal" }); + i += oct[0].length; + continue; + } + } + const num = NUM_RE.exec(rest); + if (num && (ch >= "0" && ch <= "9")) { + const raw = num[0]; + out.push({ type: "num", raw, start: i, end: i + raw.length, value: new Decimal(raw.replace(/,/g, "")), repr: "decimal" }); + i += raw.length; + continue; + } + if (ch === "." && /^\.\d/.test(rest)) { + const m = /^\.\d+/.exec(rest)!; + out.push({ type: "num", raw: m[0], start: i, end: i + m[0].length, value: new Decimal(m[0]), repr: "decimal" }); + i += m[0].length; + continue; + } + + const word = WORD_RE.exec(rest); + if (word) { + out.push({ type: "word", raw: word[0], start: i, end: i + word[0].length }); + i += word[0].length; + continue; + } + + out.push({ type: "sym", raw: ch, start: i, end: i + 1 }); + i++; + } + return out; +} + +/** Lexemes of a vocabulary phrase (positions are irrelevant). */ +export function lexPhrase(phrase: string): string[] { + return lexLine(phrase).map((l) => l.raw); +} diff --git a/src/engine/parser.ts b/src/engine/parser.ts new file mode 100644 index 0000000..7de7896 --- /dev/null +++ b/src/engine/parser.ts @@ -0,0 +1,389 @@ +// Noise-tolerant Pratt parser. Unknown words are dropped (free text around +// math is allowed), except right after a conversion word — those may name +// a timezone ("time in New York"). +import { Decimal, NumeralRepr, Unit } from "./types"; +import { Token } from "./tokenizer"; +import { PctOp, DateWord, BitOp } from "./registry"; + +export type ConvTarget = + | { type: "unit"; unit: Unit } + | { type: "currency"; code: string } + | { type: "repr"; repr: NumeralRepr } + | { type: "tz"; words: string[] } + | { type: "scale"; mult: Decimal; label: string } + | { type: "unix" } + | { type: "todate" }; + +export type Node = + | { k: "num"; v: Decimal; repr: NumeralRepr } + | { k: "const"; name: "pi" | "e" } + | { k: "var"; name: string } + | { k: "date"; word: DateWord } + | { k: "agg"; name: "sum" | "avg" | "prev" } + | { k: "bin"; op: "plus" | "minus" | "mul" | "div" | "mod" | "pow"; l: Node; r: Node } + | { k: "bit"; op: BitOp; l: Node; r: Node } + | { k: "pctop"; op: PctOp; l: Node; r: Node } + | { k: "neg"; x: Node } + | { k: "pct"; x: Node } + | { k: "fact"; x: Node } + | { k: "unit"; x: Node; unit: Unit } + | { k: "curr"; x: Node; code: string } + | { k: "scale"; x: Node; mult: Decimal; label: string } + | { k: "conv"; x: Node; target: ConvTarget } + | { k: "call"; name: string; args: Node[] } + | { k: "seq"; items: Node[] }; + +export interface ParsedLine { + assign?: string; + expr: Node | null; +} + +export function parseLine(tokens: Token[], knownVars: Set): ParsedLine { + let assign: string | undefined; + let toks = tokens; + + // `x = expr` / `x is expr` + if (toks.length >= 2 && toks[0].t === "word" && toks[1].t === "assign") { + assign = toks[0].raw; + toks = toks.slice(2); + } + + // Noise filtering: drop junk and unknown words, but keep words that follow + // a conversion operator (potential timezone names). + const filtered: Token[] = []; + let afterConv = false; + for (const tk of toks) { + if (tk.t === "junk") continue; + if (tk.t === "word") { + if (knownVars.has(tk.raw) || afterConv) filtered.push(tk); + continue; + } + afterConv = tk.t === "conv"; + filtered.push(tk); + } + + const p = new Parser(filtered, knownVars); + return { assign, expr: p.parseSeq() }; +} + +class Parser { + private i = 0; + constructor(private toks: Token[], private vars: Set) {} + + private peek(): Token | undefined { + return this.toks[this.i]; + } + private next(): Token | undefined { + return this.toks[this.i++]; + } + + /** + * Line level: a sequence of expressions with line-wide conversions. + * "2 hours 30 minutes in minutes" converts the merged sum, not the last item. + */ + parseSeq(): Node | null { + let items: Node[] = []; + const mk = (): Node => (items.length === 1 ? items[0] : { k: "seq", items }); + while (this.i < this.toks.length) { + const tk = this.peek()!; + if (tk.t === "conv" && items.length > 0) { + const save = this.i; + this.i++; + const target = this.parseTarget(); + if (target) { + items = [{ k: "conv", x: mk(), target }]; + continue; + } + this.i = save + 1; // drop the conversion word, it was noise + continue; + } + if (tk.t === "pctop" && (tk.op.startsWith("as_pct") || tk.op.endsWith("what_is")) && items.length > 0) { + this.i++; + const r = this.parseAdd(); + if (r) { + items = [{ k: "pctop", op: tk.op, l: mk(), r }]; + continue; + } + continue; + } + const before = this.i; + const node = this.parseBit(); + if (node) items.push(node); + if (this.i === before) this.i++; // skip unparsable token + } + if (items.length === 0) return null; + return mk(); + } + + /** bitwise level (one flat precedence, left-assoc): & | xor << >> */ + private parseBit(): Node | null { + let l = this.parseAdd(); + if (!l) return null; + for (;;) { + const tk = this.peek(); + if (tk?.t !== "bitop") break; + const save = this.i; + this.i++; + const r = this.parseAdd(); + if (!r) { + this.i = save; + break; + } + l = { k: "bit", op: tk.op, l, r }; + } + return l; + } + + /** conversion level: `x in cm`, `x as a % of y`, `x of what is y` */ + private parseConv(): Node | null { + let l = this.parseBit(); + if (!l) return null; + for (;;) { + const tk = this.peek(); + if (tk?.t === "conv") { + const save = this.i; + this.i++; + const target = this.parseTarget(); + if (!target) { + this.i = save; + break; + } + l = { k: "conv", x: l, target }; + } else if (tk?.t === "pctop" && (tk.op.startsWith("as_pct") || tk.op.endsWith("what_is"))) { + this.i++; + const r = this.parseAdd(); + if (!r) break; + l = { k: "pctop", op: tk.op, l, r }; + } else { + break; + } + } + return l; + } + + private parseTarget(): ConvTarget | null { + const tk = this.peek(); + if (!tk) return null; + if (tk.t === "unit") { + this.i++; + return { type: "unit", unit: tk.unit }; + } + if (tk.t === "currency") { + this.i++; + return { type: "currency", code: tk.code }; + } + if (tk.t === "repr") { + this.i++; + return { type: "repr", repr: tk.repr }; + } + if (tk.t === "special") { + this.i++; + return tk.name === "unix" ? { type: "unix" } : { type: "todate" }; + } + if (tk.t === "scale") { + this.i++; + return { type: "scale", mult: tk.mult, label: "" }; + } + if (tk.t === "date" && tk.word === "local") { + this.i++; + return { type: "tz", words: ["local"] }; + } + if (tk.t === "word") { + const words: string[] = []; + while (this.peek()?.t === "word" && words.length < 4) { + words.push((this.next() as Token & { raw: string }).raw); + } + if (words.length > 0) return { type: "tz", words }; + } + return null; + } + + private parseAdd(): Node | null { + let l = this.parseMul(); + if (!l) return null; + for (;;) { + const tk = this.peek(); + if (tk?.t === "op" && (tk.op === "plus" || tk.op === "minus")) { + const save = this.i; + this.i++; + const r = this.parseMul(); + if (!r) { + this.i = save; + break; + } + l = { k: "bin", op: tk.op, l, r }; + } else { + break; + } + } + return l; + } + + private parseMul(): Node | null { + let l = this.parseUnary(); + if (!l) return null; + for (;;) { + const tk = this.peek(); + if (tk?.t === "op" && (tk.op === "mul" || tk.op === "div" || tk.op === "mod")) { + const save = this.i; + this.i++; + const r = this.parseUnary(); + if (!r) { + this.i = save; + break; + } + l = { k: "bin", op: tk.op, l, r }; + } else if (tk?.t === "pctop" && (tk.op === "of" || tk.op === "off" || tk.op === "on")) { + // of/off/on only make sense with a percent on the left ("20% of x"); + // otherwise the word is prose noise ("5 on coffee") + if (!(l.k === "pct" || l.k === "var" || l.k === "pctop")) { + this.i++; + continue; + } + const save = this.i; + this.i++; + const r = this.parseUnary(); + if (!r) { + this.i = save; + break; + } + l = { k: "pctop", op: tk.op, l, r }; + } else if ( + tk && + (tk.t === "lparen" || tk.t === "const" || tk.t === "func" || (tk.t === "word" && this.vars.has(tk.raw))) + ) { + // implicit multiplication: 2(3+4), 2pi, 2x + const save = this.i; + const r = this.parseUnary(); + if (!r) { + this.i = save; + break; + } + l = { k: "bin", op: "mul", l, r }; + } else { + break; + } + } + return l; + } + + private parseUnary(): Node | null { + const tk = this.peek(); + if (tk?.t === "op" && tk.op === "minus") { + this.i++; + const x = this.parseUnary(); + return x ? { k: "neg", x } : null; + } + if (tk?.t === "op" && tk.op === "plus") { + this.i++; + return this.parseUnary(); + } + return this.parsePostfix(); + } + + private parsePostfix(): Node | null { + let x = this.parsePrimary(); + if (!x) return null; + for (;;) { + const tk = this.peek(); + if (!tk) break; + if (tk.t === "percent") { + this.i++; + x = { k: "pct", x }; + } else if (tk.t === "bang") { + this.i++; + x = { k: "fact", x }; + } else if (tk.t === "unit") { + this.i++; + x = { k: "unit", x, unit: tk.unit }; + } else if (tk.t === "currency") { + this.i++; + x = { k: "curr", x, code: tk.code }; + } else if (tk.t === "scale") { + this.i++; + x = { k: "scale", x, mult: tk.mult, label: "" }; + } else if (tk.t === "op" && tk.op === "pow") { + this.i++; + const r = this.parseUnary(); + if (!r) break; + x = { k: "bin", op: "pow", l: x, r }; + } else { + break; + } + } + return x; + } + + private parsePrimary(): Node | null { + const tk = this.peek(); + if (!tk) return null; + switch (tk.t) { + case "num": + this.i++; + return { k: "num", v: tk.v, repr: tk.repr }; + case "const": + this.i++; + return { k: "const", name: tk.name }; + case "date": + this.i++; + return { k: "date", word: tk.word }; + case "agg": + this.i++; + return { k: "agg", name: tk.name }; + case "word": + if (this.vars.has(tk.raw)) { + this.i++; + return { k: "var", name: tk.raw }; + } + return null; + case "func": { + this.i++; + const name = tk.name; + if (this.peek()?.t === "lparen") { + this.i++; + const args: Node[] = []; + for (;;) { + const arg = this.parseConv(); + if (arg) args.push(arg); + const nx = this.peek(); + if (nx?.t === "semicolon") { + this.i++; + continue; + } + if (nx?.t === "rparen") { + this.i++; + break; + } + if (!nx) break; + if (!arg) { + this.i++; // skip stray token inside call + } + } + return { k: "call", name, args }; + } + // `sqrt 16` / `square root of 16` — the "of" is decorative + const nx = this.peek(); + if (nx?.t === "pctop" && nx.op === "of") this.i++; + const arg = this.parseUnary(); + return arg ? { k: "call", name, args: [arg] } : null; + } + case "lparen": { + this.i++; + const inner = this.parseConv(); + if (this.peek()?.t === "rparen") this.i++; + return inner; + } + case "currency": { + // prefix currency: $9 + const nxt = this.toks[this.i + 1]; + if (nxt?.t === "num") { + this.i += 2; + return { k: "curr", x: { k: "num", v: nxt.v, repr: nxt.repr }, code: tk.code }; + } + return null; + } + default: + return null; + } + } +} diff --git a/src/engine/rates-snapshot.json b/src/engine/rates-snapshot.json new file mode 100644 index 0000000..7fec6f6 --- /dev/null +++ b/src/engine/rates-snapshot.json @@ -0,0 +1,33 @@ +{ + "date": "2026-06-01", + "base": "USD", + "rates": { + "USD": 1, "EUR": 0.92, "GBP": 0.78, "JPY": 155, "CNY": 7.2, "CHF": 0.88, + "CAD": 1.36, "AUD": 1.5, "NZD": 1.64, "RUB": 92, "UAH": 41, "BYN": 3.27, + "KZT": 470, "TRY": 38, "INR": 84, "BRL": 5.4, "MXN": 18.5, "KRW": 1370, + "SGD": 1.34, "HKD": 7.8, "TWD": 32, "THB": 35, "VND": 25400, "IDR": 16200, + "MYR": 4.6, "PHP": 57, "PLN": 3.95, "CZK": 23, "HUF": 360, "RON": 4.6, + "BGN": 1.8, "SEK": 10.5, "NOK": 10.7, "DKK": 6.9, "ISK": 138, "ILS": 3.7, + "AED": 3.6725, "SAR": 3.75, "QAR": 3.64, "KWD": 0.31, "BHD": 0.376, + "OMR": 0.3845, "JOD": 0.709, "EGP": 48, "ZAR": 18.2, "NGN": 1550, + "KES": 130, "GHS": 15.5, "MAD": 9.9, "DZD": 134, "TND": 3.1, "BTC": 0.0000095, + "AFN": 71, "ALL": 92, "AMD": 388, "ANG": 1.79, "AOA": 900, "ARS": 1050, + "AWG": 1.79, "AZN": 1.7, "BAM": 1.8, "BBD": 2, "BDT": 118, "BIF": 2900, + "BMD": 1, "BND": 1.34, "BOB": 6.9, "BSD": 1, "BTN": 84, "BWP": 13.5, + "BYR": 32700, "BZD": 2, "CDF": 2800, "CLP": 940, "COP": 4100, "CRC": 515, + "CUP": 24, "CVE": 101, "DJF": 178, "DOP": 60, "ERN": 15, "ETB": 120, + "FJD": 2.25, "FKP": 0.78, "GEL": 2.75, "GIP": 0.78, "GMD": 70, "GNF": 8600, + "GTQ": 7.75, "GYD": 209, "HNL": 25, "HRK": 6.93, "HTG": 132, "IQD": 1310, + "IRR": 42000, "JMD": 156, "KGS": 87, "KHR": 4050, "KMF": 452, "KPW": 900, + "KYD": 0.83, "LAK": 21500, "LBP": 89500, "LKR": 300, "LRD": 192, "LSL": 18.2, + "LTL": 3.18, "LVL": 0.65, "LYD": 4.85, "MDL": 17.7, "MGA": 4550, "MKD": 56.5, + "MMK": 2100, "MNT": 3400, "MOP": 8.03, "MRO": 357, "MUR": 46, "MVR": 15.4, + "MWK": 1730, "MZN": 64, "NAD": 18.2, "NIO": 36.8, "NPR": 134, "PAB": 1, + "PEN": 3.75, "PGK": 3.9, "PKR": 278, "PYG": 7600, "RSD": 107.5, "RWF": 1350, + "SBD": 8.4, "SCR": 14, "SDG": 600, "SHP": 0.78, "SLL": 22500, "SOS": 571, + "SRD": 33, "SSP": 1800, "STD": 22600, "SVC": 8.75, "SYP": 13000, "SZL": 18.2, + "TJS": 10.6, "TMT": 3.5, "TOP": 2.36, "TTD": 6.8, "TZS": 2650, "UGX": 3700, + "UYU": 41, "UZS": 12700, "VEF": 3600000, "VUV": 119, "WST": 2.75, + "XAF": 603, "XCD": 2.7, "XOF": 603, "XPF": 110, "YER": 250, "ZMW": 26 + } +} diff --git a/src/engine/registry.ts b/src/engine/registry.ts new file mode 100644 index 0000000..09b6717 --- /dev/null +++ b/src/engine/registry.ts @@ -0,0 +1,394 @@ +// Phrase registry: maps every phrase the engine understands (units, +// currencies, word-operators, functions, scales, date words) to a payload +// for the tokenizer. Longest phrase wins; case-sensitive entries (symbols +// like "m"/"M", "kB") are tried before the case-insensitive ones. +import { Decimal, Dimension, NumeralRepr, Unit, Value } from "./types"; +import { lexPhrase } from "./lexer"; +import * as vocab from "./vocab"; +import { UNIT_DATA, SI_PREFIXES, DATA_SI_PREFIXES, IEC_PREFIXES, SCALE_DATA } from "./unitdata"; +import { EXTRA_UNITS, CRYPTO } from "./extraunits"; + +export type PctOp = + | "of" | "off" | "on" + | "as_pct_of" | "as_pct_off" | "as_pct_on" + | "of_what_is" | "off_what_is" | "on_what_is"; + +export type DateWord = "today" | "tomorrow" | "yesterday" | "now" | "time" | "local"; + +export type BitOp = "band" | "bor" | "bxor" | "shl" | "shr"; + +export type Payload = + | { t: "unit"; unit: Unit } + | { t: "currency"; code: string } + | { t: "op"; op: "plus" | "minus" | "mul" | "div" | "mod" } + | { t: "bitop"; op: BitOp } + | { t: "conv" } + | { t: "assign" } + | { t: "pctop"; op: PctOp } + | { t: "percent" } + | { t: "func"; name: string } + | { t: "agg"; name: "sum" | "avg" | "prev" } + | { t: "scale"; mult: Decimal } + | { t: "repr"; repr: NumeralRepr } + | { t: "special"; name: "unix" | "todate" } + | { t: "date"; word: DateWord } + | { t: "const"; name: "pi" | "e" }; + +interface PhraseEntry { + lexemes: string[]; // raw, as in the phrase + lower: string[]; + caseSensitive: boolean; + payload: Payload; +} + +export type ExtensionFunc = (args: Value[]) => Value; + +export interface Completion { + label: string; + type: "unit" | "currency" | "function" | "keyword" | "variable"; + detail?: string; +} + +const BUILTIN_FUNCS = [ + "sqrt", "cbrt", "sin", "cos", "tan", "cot", "asin", "acos", "atan", + "arcsin", "arccos", "arctan", "sinh", "cosh", "tanh", + "ln", "lg", "log", "log2", "exp", "abs", "round", "ceil", "floor", + "fact", "factorial", +]; + +export class Registry { + /** key: lowercased first lexeme */ + private byFirst = new Map(); + unitsById = new Map(); + currencyCodes = new Set(); + currencyFormat = new Map(); + /** code -> currency units per 1 USD */ + rates = new Map(); + customFuncs = new Map(); + completions: Completion[] = []; + + addPhrase(phrase: string, payload: Payload, opts: { caseSensitive?: boolean } = {}): void { + const lexemes = lexPhrase(phrase); + if (lexemes.length === 0) return; + const caseSensitive = opts.caseSensitive ?? lexemes.every((l) => l.length <= 2); + const entry: PhraseEntry = { lexemes, lower: lexemes.map((l) => l.toLowerCase()), caseSensitive, payload }; + const key = entry.lower[0]; + const list = this.byFirst.get(key); + if (list) { + // first registration of an identical phrase wins (core vocab is added first) + if (list.some((e) => e.caseSensitive === caseSensitive && (caseSensitive ? e.lexemes.join(" ") === lexemes.join(" ") : e.lower.join(" ") === entry.lower.join(" ")))) { + return; + } + list.push(entry); + list.sort((a, b) => b.lexemes.length - a.lexemes.length || Number(b.caseSensitive) - Number(a.caseSensitive)); + } else { + this.byFirst.set(key, [entry]); + } + } + + /** + * Match the longest phrase at position i of the lexeme stream. + * rawAt/lowerAt give the lexemes of the input line. + */ + match(raws: string[], lowers: string[], i: number): { payload: Payload; length: number } | null { + const list = this.byFirst.get(lowers[i]); + if (!list) return null; + for (const e of list) { + if (i + e.lexemes.length > raws.length) continue; + let ok = true; + for (let k = 0; k < e.lexemes.length; k++) { + if (e.caseSensitive ? raws[i + k] !== e.lexemes[k] : lowers[i + k] !== e.lower[k]) { + ok = false; + break; + } + } + if (ok) return { payload: e.payload, length: e.lexemes.length }; + } + return null; + } + + setRates(rates: Record): void { + this.rates.clear(); + for (const [code, v] of Object.entries(rates)) { + const d = new Decimal(v); + if (d.isFinite() && d.gt(0)) this.rates.set(code.toUpperCase(), d); + } + this.rates.set("USD", new Decimal(1)); + } + + makeCurrencyUnit(code: string): Unit | null { + const rate = this.rates.get(code); + if (!rate) return null; + return { + id: code, + dimension: "currency", + ratio: new Decimal(1).div(rate), // USD per 1 unit + format: this.currencyFormat.get(code) ?? `{} ${code}`, + }; + } + + isFunc(name: string): boolean { + return BUILTIN_FUNCS.includes(name) || this.customFuncs.has(name); + } +} + +function addUnitPhrases(reg: Registry, phrases: string[], unit: Unit, opts: { caseSensitive?: boolean } = {}): void { + reg.unitsById.set(unit.id, unit); + for (const p of phrases) reg.addPhrase(p, { t: "unit", unit }, opts); +} + +export function buildRegistry(): Registry { + const reg = new Registry(); + + // operators, percent, dates, numerals, scales, aggregates + const ops: Array<[string, Payload]> = [ + ["plus", { t: "op", op: "plus" }], + ["minus", { t: "op", op: "minus" }], + ["multiply", { t: "op", op: "mul" }], + ["divide", { t: "op", op: "div" }], + ]; + for (const [key, payload] of ops) { + for (const v of vocab.variants("Operations", `${key}.variants`)) reg.addPhrase(v, payload, { caseSensitive: false }); + } + for (const v of vocab.variants("Operations", "conversion.variants")) reg.addPhrase(v, { t: "conv" }, { caseSensitive: false }); + for (const v of vocab.variants("Operations", "assignment.variants")) reg.addPhrase(v, { t: "assign" }, { caseSensitive: false }); + for (const v of vocab.variants("Operations", "sum.variants")) reg.addPhrase(v, { t: "agg", name: "sum" }, { caseSensitive: false }); + for (const v of vocab.variants("Operations", "avg.variants")) reg.addPhrase(v, { t: "agg", name: "avg" }, { caseSensitive: false }); + for (const v of vocab.variants("Operations", "prev.variants")) reg.addPhrase(v, { t: "agg", name: "prev" }, { caseSensitive: false }); + for (const v of vocab.variants("Operations", "sqrt.variants")) reg.addPhrase(v, { t: "func", name: "sqrt" }, { caseSensitive: false }); + for (const v of vocab.variants("Operations", "cbrt.variants")) reg.addPhrase(v, { t: "func", name: "cbrt" }, { caseSensitive: false }); + reg.addPhrase("mod", { t: "op", op: "mod" }, { caseSensitive: false }); + reg.addPhrase("modulo", { t: "op", op: "mod" }, { caseSensitive: false }); + + // bitwise: "and" stays an addition word (original Numi), so & | xor shl shr + reg.addPhrase("xor", { t: "bitop", op: "bxor" }, { caseSensitive: false }); + reg.addPhrase("or", { t: "bitop", op: "bor" }, { caseSensitive: false }); + reg.addPhrase("shl", { t: "bitop", op: "shl" }, { caseSensitive: false }); + reg.addPhrase("shr", { t: "bitop", op: "shr" }, { caseSensitive: false }); + + // unix timestamps and extra numeral representations + for (const v of ["unix", "unixtime", "unix time", "timestamp", "таймстамп"]) { + reg.addPhrase(v, { t: "special", name: "unix" }, { caseSensitive: false }); + } + for (const v of ["date", "datetime", "дата", "дату", "датой"]) { + reg.addPhrase(v, { t: "special", name: "todate" }, { caseSensitive: false }); + } + for (const v of ["fraction", "fractions", "дробь", "дробью", "дроби"]) { + reg.addPhrase(v, { t: "repr", repr: "fraction" }, { caseSensitive: false }); + } + for (const v of ["roman", "roman numerals", "римские", "римскими"]) { + reg.addPhrase(v, { t: "repr", repr: "roman" }, { caseSensitive: false }); + } + + for (const v of vocab.variants("Percent", "percent.variants")) reg.addPhrase(v, { t: "percent" }); + const pctOps: Array<[string, PctOp]> = [ + ["percent_of", "of"], ["percent_off", "off"], ["percent_on", "on"], + ["as_a_percent_of", "as_pct_of"], ["as_a_percent_off", "as_pct_off"], ["as_a_percent_on", "as_pct_on"], + ["percent_of_what_is", "of_what_is"], ["percent_off_what_is", "off_what_is"], ["percent_on_what_is", "on_what_is"], + ]; + for (const [key, op] of pctOps) { + for (const v of vocab.variants("Percent", `${key}.variants`)) reg.addPhrase(v, { t: "pctop", op }, { caseSensitive: false }); + } + + const dateWords: Array<[string, DateWord]> = [ + ["today", "today"], ["tomorrow", "tomorrow"], ["yesterday", "yesterday"], + ]; + for (const [key, word] of dateWords) { + for (const v of vocab.variants("Dates", `${key}.variants`)) reg.addPhrase(v, { t: "date", word }, { caseSensitive: false }); + } + for (const v of vocab.variants("Dates", "current_time.variants")) reg.addPhrase(v, { t: "date", word: v.toLowerCase() === "now" || v.toLowerCase() === "сейчас" ? "now" : "time" }, { caseSensitive: false }); + for (const v of vocab.variants("Dates", "local_time.variants")) reg.addPhrase(v, { t: "date", word: "local" }, { caseSensitive: false }); + + const reprs: Array<[string, NumeralRepr]> = [ + ["hex", "hex"], ["binary", "binary"], ["octal", "octal"], ["decimal", "decimal"], ["scientific", "scientific"], + ]; + for (const [key, repr] of reprs) { + for (const v of vocab.variants("Numbers", `${key}.variants`)) reg.addPhrase(v, { t: "repr", repr }, { caseSensitive: false }); + } + + for (const [id, mult] of Object.entries(SCALE_DATA)) { + for (const v of vocab.variants("Scales", `${id}.variants`)) { + reg.addPhrase(v, { t: "scale", mult: new Decimal(mult) }); + } + } + + reg.addPhrase("pi", { t: "const", name: "pi" }, { caseSensitive: false }); + reg.addPhrase("π", { t: "const", name: "pi" }); + reg.addPhrase("пи", { t: "const", name: "pi" }, { caseSensitive: false }); + reg.addPhrase("e", { t: "const", name: "e" }, { caseSensitive: true }); + + for (const f of BUILTIN_FUNCS) reg.addPhrase(f, { t: "func", name: f }, { caseSensitive: false }); + + // units, SI/IEC prefixes, area/volume templates + interface LengthPhrase { phrase: string; unit: Unit; caseSensitive?: boolean } + const lengthPhrases: LengthPhrase[] = []; + + for (const d of UNIT_DATA) { + const variants = vocab.variants(d.category, `${d.id}.variants`); + const format = vocab.entryEn(d.category, `${d.id}.format`) ?? d.format ?? d.id; + const symbol = vocab.entryEn(d.category, `${d.id}.symbol`); + const symbolsAll = vocab.entriesAll(d.category, `${d.id}.symbol`); + const unit: Unit = { + id: d.id, + dimension: d.dimension, + ratio: new Decimal(d.ratio), + ...(d.offset ? { offset: new Decimal(d.offset) } : {}), + format, + }; + addUnitPhrases(reg, variants, unit); + if (d.dimension === "length") { + for (const v of variants) lengthPhrases.push({ phrase: v, unit }); + } + + if (d.prefixes) { + let prefixes = d.prefixes === "si" ? SI_PREFIXES : [...DATA_SI_PREFIXES, ...IEC_PREFIXES]; + if (d.id === "second") { + // only sub-second prefixes are useful; "as" (attosecond) would + // shadow the conversion word "as", "das"/"hs" are noise + prefixes = prefixes.filter((p) => ["milli", "micro", "nano", "pico"].includes(p.id)); + } + const wordVariants = variants.filter((v) => v.length > 2 && /^[\p{L}]+$/u.test(v)); + for (const p of prefixes) { + const pWords = vocab.variants(p.category, `${p.id}.prefix`); + const pSym = vocab.entryEn(p.category, `${p.id}.symbol`) ?? ""; + const pSymsAll = vocab.entriesAll(p.category, `${p.id}.symbol`); + const mult = new Decimal(p.mult); + const pUnit: Unit = { + id: `${p.id}:${d.id}`, + dimension: d.dimension, + ratio: unit.ratio.mul(mult), + format: pSym + (symbol ?? format), + }; + const phrases: string[] = []; + const csPhrases: string[] = []; + if (d.id === "bit") { + // no "kb" for bits — bare *b symbols mean bytes (common expectation) + csPhrases.push(`${pSym}bit`); + } else { + // symbol compositions in every locale: "km" and "км" + for (const ps of pSymsAll) for (const us of symbolsAll) csPhrases.push(ps + us); + } + for (const pw of pWords) for (const wv of wordVariants) phrases.push(pw + wv); + if (d.id === "byte" && pSym) { + // lenient: KB/kb/Kb all mean kilobyte + phrases.push((pSym + "b").toLowerCase()); + } + reg.unitsById.set(pUnit.id, pUnit); + for (const ph of csPhrases) reg.addPhrase(ph, { t: "unit", unit: pUnit }, { caseSensitive: true }); + for (const ph of phrases) reg.addPhrase(ph, { t: "unit", unit: pUnit }, { caseSensitive: false }); + if (d.dimension === "length") { + for (const ph of csPhrases) lengthPhrases.push({ phrase: ph, unit: pUnit, caseSensitive: true }); + for (const pw of pWords) for (const wv of wordVariants) lengthPhrases.push({ phrase: pw + wv, unit: pUnit, caseSensitive: false }); + } + } + } + } + + // Area/volume from templates: "square %@" / "cubic %@" applied to every length phrase + const applyTemplates = ( + templates: string[], + fmtTemplate: string, + dim: Dimension, + pow: 2 | 3, + idPrefix: string, + ) => { + for (const t of templates) { + for (const lp of lengthPhrases) { + const phrase = t.replace("%@", lp.phrase); + const id = `${idPrefix}:${lp.unit.id}`; + let u = reg.unitsById.get(id); + if (!u) { + u = { + id, + dimension: dim, + ratio: lp.unit.ratio.pow(pow), + format: fmtTemplate.replace("%@", lp.unit.format), + }; + reg.unitsById.set(id, u); + } + reg.addPhrase(phrase, { t: "unit", unit: u }, lp.caseSensitive !== undefined ? { caseSensitive: lp.caseSensitive } : {}); + } + } + }; + applyTemplates(vocab.variants("Area", "area.templates"), vocab.entryEn("Area", "area.format") ?? "%@²", "area", 2, "sq"); + applyTemplates(vocab.variants("Volume", "volume.templates"), vocab.entryEn("Volume", "volume.format") ?? "%@³", "volume", 3, "cb"); + + // currencies + // Major currencies register first so they win generic phrases + // ("доллар" belongs to USD, not TTD which lists it too and sorts earlier). + const PRIORITY = [ + "USD", "EUR", "GBP", "RUB", "JPY", "CNY", "CHF", "CAD", "AUD", "UAH", + "BYN", "KZT", "PLN", "TRY", "INR", "BRL", "KRW", "NZD", "SEK", "NOK", + "DKK", "CZK", "ILS", "HKD", "SGD", "BTC", + ]; + const codes = vocab.idsOf("Currency").sort((a, b) => { + const pa = PRIORITY.indexOf(a); + const pb = PRIORITY.indexOf(b); + return (pa === -1 ? 999 : pa) - (pb === -1 ? 999 : pb) || a.localeCompare(b); + }); + for (const code of codes) { + reg.currencyCodes.add(code); + const fmt = vocab.entryEn("Currency", `${code}.format`); + if (fmt) reg.currencyFormat.set(code, fmt.replace("%@", "{}")); + const variants = vocab.variants("Currency", `${code}.variants`); + if (!variants.some((v) => v.toUpperCase() === code)) variants.push(code); + for (const v of variants) reg.addPhrase(v, { t: "currency", code }, { caseSensitive: false }); + } + + // speed, pressure, energy, power, frequency, fuel + for (const d of EXTRA_UNITS) { + const unit: Unit = { + id: d.id, + dimension: d.dimension, + ratio: new Decimal(d.ratio), + ...(d.reciprocal ? { reciprocal: true } : {}), + format: d.format, + }; + reg.unitsById.set(unit.id, unit); + for (const p of d.phrases.split(",")) { + const ph = p.trim(); + if (ph) reg.addPhrase(ph, { t: "unit", unit }, { caseSensitive: false }); + } + if (d.symbols) { + for (const s of d.symbols.split(",")) { + const ph = s.trim(); + if (ph) reg.addPhrase(ph, { t: "unit", unit }, { caseSensitive: true }); + } + } + } + + // cryptocurrencies (live prices come from the shell) + // codes that collide with common words match case-sensitively only + const CRYPTO_STRICT_CODES = new Set(["TON", "NEAR", "ATOM", "LINK", "UNI", "DOT", "SOL"]); + for (const c of CRYPTO) { + reg.currencyCodes.add(c.code); + reg.currencyFormat.set(c.code, `{} ${c.code}`); + reg.addPhrase(c.code, { t: "currency", code: c.code }, { caseSensitive: CRYPTO_STRICT_CODES.has(c.code) }); + for (const p of c.phrases.split(",")) { + const ph = p.trim(); + if (ph) reg.addPhrase(ph, { t: "currency", code: c.code }, { caseSensitive: false }); + } + } + + // autocomplete entries + const seen = new Set(); + const addCompletion = (label: string, type: Completion["type"], detail?: string) => { + const k = `${type}:${label.toLowerCase()}`; + if (seen.has(k) || label.length < 2 || !/^[\p{L}]/u.test(label)) return; + seen.add(k); + reg.completions.push({ label, type, detail }); + }; + for (const d of UNIT_DATA) { + for (const v of vocab.variants(d.category, `${d.id}.variants`)) addCompletion(v, "unit", d.dimension); + } + for (const d of EXTRA_UNITS) { + for (const v of d.phrases.split(",")) addCompletion(v.trim(), "unit", d.dimension); + } + for (const code of reg.currencyCodes) addCompletion(code, "currency"); + for (const f of BUILTIN_FUNCS) addCompletion(f, "function"); + for (const k of ["sum", "total", "average", "avg", "prev", "today", "tomorrow", "yesterday", "time", "now", "hex", "binary"]) { + addCompletion(k, "keyword"); + } + + return reg; +} diff --git a/src/engine/tokenizer.ts b/src/engine/tokenizer.ts new file mode 100644 index 0000000..be182f8 --- /dev/null +++ b/src/engine/tokenizer.ts @@ -0,0 +1,128 @@ +// Lexes a line and resolves phrases through the registry into semantic tokens. +import { Decimal, NumeralRepr, Unit } from "./types"; +import { lexLine } from "./lexer"; +import { Registry, PctOp, DateWord, BitOp } from "./registry"; + +export type Token = + | { t: "num"; v: Decimal; repr: NumeralRepr; start: number; end: number } + | { t: "unit"; unit: Unit; start: number; end: number } + | { t: "currency"; code: string; start: number; end: number } + | { t: "op"; op: "plus" | "minus" | "mul" | "div" | "mod" | "pow"; start: number; end: number } + | { t: "bitop"; op: BitOp; start: number; end: number } + | { t: "special"; name: "unix" | "todate"; start: number; end: number } + | { t: "conv"; start: number; end: number } + | { t: "assign"; start: number; end: number } + | { t: "pctop"; op: PctOp; start: number; end: number } + | { t: "percent"; start: number; end: number } + | { t: "func"; name: string; start: number; end: number } + | { t: "agg"; name: "sum" | "avg" | "prev"; start: number; end: number } + | { t: "scale"; mult: Decimal; start: number; end: number } + | { t: "repr"; repr: NumeralRepr; start: number; end: number } + | { t: "date"; word: DateWord; start: number; end: number } + | { t: "const"; name: "pi" | "e"; start: number; end: number } + | { t: "lparen"; start: number; end: number } + | { t: "rparen"; start: number; end: number } + | { t: "semicolon"; start: number; end: number } + | { t: "bang"; start: number; end: number } + | { t: "word"; raw: string; start: number; end: number } + | { t: "junk"; raw: string; start: number; end: number }; + +const SYM_OPS: Record = {}; + +export function tokenize(line: string, reg: Registry): Token[] { + void SYM_OPS; + const lexes = lexLine(line); + const raws = lexes.map((l) => l.raw); + const lowers = raws.map((r) => r.toLowerCase()); + const tokens: Token[] = []; + let i = 0; + while (i < lexes.length) { + const lx = lexes[i]; + const span = { start: lx.start, end: lx.end }; + + if (lx.type === "num") { + tokens.push({ t: "num", v: lx.value!, repr: lx.repr!, ...span }); + i++; + continue; + } + + const m = reg.match(raws, lowers, i); + if (m) { + const end = lexes[i + m.length - 1].end; + const s = { start: lx.start, end }; + const p = m.payload; + switch (p.t) { + case "unit": tokens.push({ t: "unit", unit: p.unit, ...s }); break; + case "currency": tokens.push({ t: "currency", code: p.code, ...s }); break; + case "op": tokens.push({ t: "op", op: p.op, ...s }); break; + case "bitop": tokens.push({ t: "bitop", op: p.op, ...s }); break; + case "special": tokens.push({ t: "special", name: p.name, ...s }); break; + case "conv": tokens.push({ t: "conv", ...s }); break; + case "assign": tokens.push({ t: "assign", ...s }); break; + case "pctop": tokens.push({ t: "pctop", op: p.op, ...s }); break; + case "percent": tokens.push({ t: "percent", ...s }); break; + case "func": tokens.push({ t: "func", name: p.name, ...s }); break; + case "agg": tokens.push({ t: "agg", name: p.name, ...s }); break; + case "scale": tokens.push({ t: "scale", mult: p.mult, ...s }); break; + case "repr": tokens.push({ t: "repr", repr: p.repr, ...s }); break; + case "date": tokens.push({ t: "date", word: p.word, ...s }); break; + case "const": tokens.push({ t: "const", name: p.name, ...s }); break; + } + i += m.length; + continue; + } + + if (lx.type === "sym") { + // << and >> arrive as two single-char lexemes + const nextLx = lexes[i + 1]; + if ((lx.raw === "<" || lx.raw === ">") && nextLx?.type === "sym" && nextLx.raw === lx.raw && nextLx.start === lx.end) { + tokens.push({ t: "bitop", op: lx.raw === "<" ? "shl" : "shr", start: lx.start, end: nextLx.end }); + i += 2; + continue; + } + switch (lx.raw) { + case "&": tokens.push({ t: "bitop", op: "band", ...span }); break; + case "|": tokens.push({ t: "bitop", op: "bor", ...span }); break; + case "+": tokens.push({ t: "op", op: "plus", ...span }); break; + case "-": case "−": case "–": tokens.push({ t: "op", op: "minus", ...span }); break; + case "*": case "×": case "·": tokens.push({ t: "op", op: "mul", ...span }); break; + case "/": case "÷": tokens.push({ t: "op", op: "div", ...span }); break; + case "^": tokens.push({ t: "op", op: "pow", ...span }); break; + case "%": tokens.push({ t: "percent", ...span }); break; + case "(": tokens.push({ t: "lparen", ...span }); break; + case ")": tokens.push({ t: "rparen", ...span }); break; + case "=": tokens.push({ t: "assign", ...span }); break; + case ";": tokens.push({ t: "semicolon", ...span }); break; + case "!": tokens.push({ t: "bang", ...span }); break; + default: tokens.push({ t: "junk", raw: lx.raw, ...span }); + } + i++; + continue; + } + + tokens.push({ t: "word", raw: lx.raw, ...span }); + i++; + } + return disambiguateIn(tokens, reg, line); +} + +/** + * "5 ft 4 in in cm" / "6 ft 3 in": the word "in" right after a number, + * followed by another conversion word, an operator or the end of the line, + * actually means inches. + */ +function disambiguateIn(tokens: Token[], reg: Registry, line: string): Token[] { + const inch = reg.unitsById.get("inch"); + if (!inch) return tokens; + return tokens.map((tk, i) => { + if (tk.t !== "conv" || line.slice(tk.start, tk.end).toLowerCase() !== "in") return tk; + const prev = tokens[i - 1]; + const next = tokens[i + 1]; + const prevIsNum = prev?.t === "num"; + const nextEnds = !next || next.t === "conv" || next.t === "op" || next.t === "rparen"; + if (prevIsNum && nextEnds) { + return { t: "unit", unit: inch, start: tk.start, end: tk.end } as Token; + } + return tk; + }); +} diff --git a/src/engine/types.ts b/src/engine/types.ts new file mode 100644 index 0000000..5527756 --- /dev/null +++ b/src/engine/types.ts @@ -0,0 +1,90 @@ +import Decimal from "decimal.js"; + +Decimal.set({ precision: 40, toExpNeg: -9, toExpPos: 21 }); + +export { Decimal }; + +export type Dimension = + | "scalar" + | "length" + | "area" + | "volume" + | "weight" + | "temperature" + | "time" + | "angle" + | "data" + | "currency" + | "speed" + | "pressure" + | "energy" + | "power" + | "frequency" + | "fuel"; + +export type NumeralRepr = "decimal" | "hex" | "binary" | "octal" | "scientific" | "fraction" | "roman" | "plain"; + +/** A resolved unit instance attached to a value. */ +export interface Unit { + /** canonical id, e.g. "meter", "kilo:meter", "USD", "sq:foot" */ + id: string; + dimension: Dimension; + /** multiplier to the dimension's base unit (meter, m², m³, gram, second, radian, bit, K, USD) */ + ratio: Decimal; + /** additive offset for temperature: base = value*ratio + offset */ + offset?: Decimal; + /** reciprocal unit (mpg vs L/100km): base = ratio / value */ + reciprocal?: boolean; + /** display format: either a template with {} or a suffix, e.g. "m", "″", "${}" */ + format: string; +} + +export interface Quantity { + kind: "quantity"; + value: Decimal; + unit: Unit | null; + repr: NumeralRepr; +} + +export interface PercentVal { + kind: "percent"; + value: Decimal; // 20% -> 20 +} + +export interface DateVal { + kind: "date"; + /** epoch ms */ + ms: number; + hasTime: boolean; + /** show only the time part ("time in Tokyo") */ + timeOnly?: boolean; + /** IANA timezone for display, defaults to local */ + timeZone?: string; +} + +export type Value = Quantity | PercentVal | DateVal; + +export function qty(value: Decimal.Value, unit: Unit | null = null, repr: NumeralRepr = "decimal"): Quantity { + return { kind: "quantity", value: new Decimal(value), unit, repr }; +} + +export function pct(value: Decimal.Value): PercentVal { + return { kind: "percent", value: new Decimal(value) }; +} + +export class EvalError extends Error {} + +export interface EngineSettings { + /** max decimal places shown */ + precision: number; + /** thousands separator: "," | " " | "" */ + groupSeparator: string; + /** decimal separator shown in output */ + decimalSeparator: string; +} + +export const defaultSettings: EngineSettings = { + precision: 2, + groupSeparator: ",", + decimalSeparator: ".", +}; diff --git a/src/engine/unitdata.ts b/src/engine/unitdata.ts new file mode 100644 index 0000000..92a1c7f --- /dev/null +++ b/src/engine/unitdata.ts @@ -0,0 +1,135 @@ +// Conversion ratios. Base units per dimension: length=meter, area=m², +// volume=m³, weight=gram, time=second, angle=radian, data=bit, +// temperature=kelvin (ratio+offset). +import { Decimal, Dimension } from "./types"; + +export interface UnitData { + id: string; + /** vocab category that holds `.variants` / `.format` */ + category: string; + dimension: Dimension; + ratio: string; + offset?: string; + /** which prefix family applies (si => mm/cm/km..., data => kilo..yotta + kibi..yobi) */ + prefixes?: "si" | "data"; + /** fallback format if vocab has none */ + format?: string; +} + +const IN = 0.0254; + +export const UNIT_DATA: UnitData[] = [ + // Length (base: meter) + { id: "meter", category: "Length", dimension: "length", ratio: "1", prefixes: "si" }, + { id: "inch", category: "Length", dimension: "length", ratio: String(IN) }, + { id: "mile", category: "Length", dimension: "length", ratio: "1609.344" }, + { id: "foot", category: "Length", dimension: "length", ratio: "0.3048" }, + { id: "yard", category: "Length", dimension: "length", ratio: "0.9144" }, + { id: "point", category: "Length", dimension: "length", ratio: String(IN / 72) }, + { id: "cable", category: "Length", dimension: "length", ratio: "185.2" }, + { id: "league", category: "Length", dimension: "length", ratio: "4828.032" }, + { id: "furlong", category: "Length", dimension: "length", ratio: "201.168" }, + { id: "nautical_mile", category: "Length", dimension: "length", ratio: "1852" }, + { id: "chain", category: "Length", dimension: "length", ratio: "20.1168" }, + // CSS lengths (reference: 96px = 1in, 1em = 16px) + { id: "pixel", category: "Css", dimension: "length", ratio: String(IN / 96) }, + { id: "em", category: "Css", dimension: "length", ratio: String(IN / 6) }, + + // Area (base: m²) + { id: "are", category: "Area", dimension: "area", ratio: "100" }, + { id: "hectare", category: "Area", dimension: "area", ratio: "10000" }, + { id: "acre", category: "Area", dimension: "area", ratio: "4046.8564224" }, + { id: "ping", category: "Area", dimension: "area", ratio: "3.30579" }, + + // Volume (base: m³) + { id: "litre", category: "Volume", dimension: "volume", ratio: "0.001", prefixes: "si" }, + { id: "cup", category: "Volume", dimension: "volume", ratio: "0.0002365882365" }, + { id: "tablespoon", category: "Volume", dimension: "volume", ratio: "0.0000147867647825" }, + { id: "teaspoon", category: "Volume", dimension: "volume", ratio: "0.00000492892159375" }, + { id: "quart", category: "Volume", dimension: "volume", ratio: "0.000946352946" }, + { id: "pint", category: "Volume", dimension: "volume", ratio: "0.000473176473" }, + { id: "gallon", category: "Volume", dimension: "volume", ratio: "0.003785411784" }, + { id: "cbinch", category: "Volume", dimension: "volume", ratio: "0.000016387064", format: "in³" }, + { id: "cbcm", category: "Volume", dimension: "volume", ratio: "0.000001", format: "cm³" }, + + // Weight (base: gram) + { id: "gram", category: "Weight", dimension: "weight", ratio: "1", prefixes: "si" }, + { id: "tonne", category: "Weight", dimension: "weight", ratio: "1000000" }, + { id: "stone", category: "Weight", dimension: "weight", ratio: "6350.29318" }, + { id: "carat", category: "Weight", dimension: "weight", ratio: "0.2" }, + { id: "pound", category: "Weight", dimension: "weight", ratio: "453.59237" }, + { id: "centner", category: "Weight", dimension: "weight", ratio: "100000" }, + { id: "ounce", category: "Weight", dimension: "weight", ratio: "28.349523125" }, + + // Temperature (base: kelvin); base = value*ratio + offset + { id: "celsius", category: "Temperature", dimension: "temperature", ratio: "1", offset: "273.15" }, + { id: "fahrenheit", category: "Temperature", dimension: "temperature", ratio: String(5 / 9), offset: String((459.67 * 5) / 9) }, + { id: "kelvin", category: "Temperature", dimension: "temperature", ratio: "1" }, + + // Time (base: second). Month = 30 days, year = 365 days here; + // date arithmetic uses real calendar months separately. + { id: "second", category: "Time", dimension: "time", ratio: "1", prefixes: "si" }, + { id: "minute", category: "Time", dimension: "time", ratio: "60" }, + { id: "hour", category: "Time", dimension: "time", ratio: "3600" }, + { id: "day", category: "Time", dimension: "time", ratio: "86400" }, + { id: "week", category: "Time", dimension: "time", ratio: "604800" }, + { id: "month", category: "Time", dimension: "time", ratio: "2592000" }, + { id: "year", category: "Time", dimension: "time", ratio: "31536000" }, + + // Angle (base: radian) + { id: "radian", category: "Angle", dimension: "angle", ratio: "1" }, + { id: "degree", category: "Angle", dimension: "angle", ratio: String(Math.PI / 180) }, + + // Data (base: bit) + { id: "bit", category: "Data", dimension: "data", ratio: "1", prefixes: "data" }, + { id: "byte", category: "Data", dimension: "data", ratio: "8", prefixes: "data" }, +]; + +export interface PrefixData { + id: string; // vocab id in SIPrefixes/IECPrefixes + category: "SIPrefixes" | "IECPrefixes"; + mult: string; +} + +export const SI_PREFIXES: PrefixData[] = [ + { id: "yotta", category: "SIPrefixes", mult: "1e24" }, + { id: "zetta", category: "SIPrefixes", mult: "1e21" }, + { id: "exa", category: "SIPrefixes", mult: "1e18" }, + { id: "peta", category: "SIPrefixes", mult: "1e15" }, + { id: "tera", category: "SIPrefixes", mult: "1e12" }, + { id: "giga", category: "SIPrefixes", mult: "1e9" }, + { id: "mega", category: "SIPrefixes", mult: "1e6" }, + { id: "kilo", category: "SIPrefixes", mult: "1e3" }, + { id: "hecto", category: "SIPrefixes", mult: "1e2" }, + { id: "deca", category: "SIPrefixes", mult: "1e1" }, + { id: "deci", category: "SIPrefixes", mult: "1e-1" }, + { id: "centi", category: "SIPrefixes", mult: "1e-2" }, + { id: "milli", category: "SIPrefixes", mult: "1e-3" }, + { id: "micro", category: "SIPrefixes", mult: "1e-6" }, + { id: "nano", category: "SIPrefixes", mult: "1e-9" }, + { id: "pico", category: "SIPrefixes", mult: "1e-12" }, + { id: "femto", category: "SIPrefixes", mult: "1e-15" }, + { id: "atto", category: "SIPrefixes", mult: "1e-18" }, + { id: "zepto", category: "SIPrefixes", mult: "1e-21" }, + { id: "yocto", category: "SIPrefixes", mult: "1e-24" }, +]; + +/** Prefixes ≥ kilo, applied to data units (no millibits). */ +export const DATA_SI_PREFIXES = SI_PREFIXES.filter((p) => new Decimal(p.mult).gte(1000)); + +export const IEC_PREFIXES: PrefixData[] = [ + { id: "kibi", category: "IECPrefixes", mult: "1024" }, + { id: "mebi", category: "IECPrefixes", mult: String(2 ** 20) }, + { id: "gibi", category: "IECPrefixes", mult: String(2 ** 30) }, + { id: "tebi", category: "IECPrefixes", mult: String(2 ** 40) }, + { id: "pebi", category: "IECPrefixes", mult: new Decimal(2).pow(50).toString() }, + { id: "exbi", category: "IECPrefixes", mult: new Decimal(2).pow(60).toString() }, + { id: "zebi", category: "IECPrefixes", mult: new Decimal(2).pow(70).toString() }, + { id: "yobi", category: "IECPrefixes", mult: new Decimal(2).pow(80).toString() }, +]; + +export const SCALE_DATA: Record = { + thousand: "1e3", + million: "1e6", + billion: "1e9", +}; diff --git a/src/engine/vocab-data.ts b/src/engine/vocab-data.ts new file mode 100644 index 0000000..d3727cd --- /dev/null +++ b/src/engine/vocab-data.ts @@ -0,0 +1,1015 @@ +// Names, spellings and display formats of units, currencies, word-operators +// and date words (en + ru). Conversion ratios live in unitdata.ts. +import type { VocabLocale } from "./vocab"; + +export const EN: VocabLocale = { + "Angle": { + "degree.format": "°", + "degree.variants": "degree,°,degrees,arcdegree", + "radian.format": "rad", + "radian.variants": "radian,rad,radians" + }, + "Area": { + "acre.format": "acre", + "acre.variants": "acre,acres", + "are.format": "a", + "are.variants": "a,are,ares", + "area.format": "%@²", + "area.templates": "square %@,sq %@,sq%@,%@²,%@ ²,%@ 2", + "hectare.format": "ha", + "hectare.variants": "hectare,hectares,ha", + "ping.format": "píng", + "ping.variants": "ping,píng,pings,píngs" + }, + "Css": { + "em.format": "em", + "em.variants": "em,ems", + "pixel.format": "px", + "pixel.variants": "pixel,pixels,px,px." + }, + "Currency": { + "AED.format": "%@ Dh", + "AED.variants": "United Arab Emirates dirham,Dirham,Dirhams,UAE Dirham,UAE Dirhams,Dhs,د.إ", + "AFN.format": "%@ Af", + "AFN.variants": "Afgani,Afgan Afgani,Afs,؋,Af", + "ALL.format": "%@ L", + "ALL.variants": "Lek,Albanian Lek,Albanian Leks", + "AMD.format": "%@ Դ", + "AMD.variants": "Armenian Dram,Dram,Drams,Armenian Drams,Դ", + "ANG.format": "ƒ %@", + "ANG.variants": "Netherlands Antillean Guilders,Netherlands Antillean Guilder,Guilder,Guilders,NAƒ,NAf,ƒ", + "AOA.format": "%@ Kz", + "AOA.variants": "Angolan Kwanzas,Angolan Kwanza,Kwanza,Kwanzas,Kz", + "ARS.format": "%@ ARS", + "ARS.variants": "Argentine Peso,Argentine Pesos", + "AUD.format": "$%@ AUD", + "AUD.variants": "Australian Dollars,Australian Dollar", + "AWG.format": "%@ Afl.", + "AWG.variants": "Aruban florin,florin,Afl.", + "AZN.format": "%@ man.", + "AZN.variants": "Azerbaijani Manats,Azerbaijani Manat,Manat,Manats,man.", + "BAM.format": "%@ KM", + "BAM.variants": "Bosnia and Herzegovina convertible mark,Bosnia and Herzegovina convertible marks,convertible mark,convertible marks,Maraka", + "BBD.format": "%@ BBD", + "BBD.variants": "Barbados Dollars,Barbados Dollar", + "BDT.format": "%@ Tk", + "BDT.variants": "Bangladeshi Takas,Bangladeshi Taka,Takas,Taka,Tk,৳", + "BGN.format": "%@ лв.", + "BGN.variants": "Bulgarian Levs,Bulgarian Lev,Lev,Leva,Bulgarian Leva,лв.", + "BHD.format": "%@ BD", + "BHD.variants": "Bahraini Dinars,Bahraini Dinar,.د.ب,BD", + "BIF.format": "%@ FBu", + "BIF.variants": "Burundi Francs,Burundi Franc,Burundian franc,Burundian francs,FBu", + "BMD.format": "$ %@", + "BMD.variants": "Bermudian Dollars,Bermudian Dollar,BD$", + "BND.format": "B$ %@", + "BND.variants": "Brunei Dollars,Brunei Dollar", + "BOB.format": "Bs %@", + "BOB.variants": "Bolivian Bolivianos,Bolivian Boliviano,Bolivianos,Boliviano,Bs", + "BRL.format": "R$ %@", + "BRL.variants": "Brazilian Reals,Brazilian Real,R$", + "BSD.format": "B$ %@", + "BSD.variants": "Bahamian Dollars,Bahamian Dollar", + "BTC.format": "%@ B⃦", + "BTC.variants": "Bitcoins,Bitcoin,B⃦,xbt", + "BTN.format": "Nu. %@", + "BTN.variants": "Bhutanese Ngultrum,Ngultrum,Bhutanese Ngultrums,Ngultrums,Nu.", + "BWP.format": "%@ BWP", + "BWP.variants": "Botswana Pula,Pula", + "BYN.format": "%@ BYN", + "BYN.variants": "Belarusian ruble,Belarusian rubles,bel. ruble,bel rubles", + "BYR.format": "%@ BYR", + "BYR.variants": "BYR", + "BZD.format": "BZ$ %@", + "BZD.variants": "Belize Dollars,Belize Dollar,BZ$", + "CAD.format": "$%@ CAD", + "CAD.variants": "Canadian Dollars,Canadian Dollar,CA$,$ CAD,$CAD", + "CDF.format": "%@ FC", + "CDF.variants": "Congolese Francs,Congolese Franc,FC", + "CHF.format": "%@ SFr.", + "CHF.variants": "Swiss Francs,Swiss Franc,SFr.", + "CLP.format": "%@ CLP", + "CLP.variants": "Chilean Pesos,Chilean Peso", + "CNY.format": "%@ ¥", + "CNY.variants": "Chinese Yuans,Chinese Yuan,yuan,yuans,元,RMB", + "COP.format": "$%@ COP", + "COP.variants": "Colombian Pesos,Colombian Peso", + "CRC.format": "%@ ₡", + "CRC.variants": "Costa Rican Colones,Costa Rican Colon,Colon,Colones", + "CUP.format": "$MN %@", + "CUP.variants": "Cuban Pesos,Cuban Peso,$MN", + "CVE.format": "%@ CVE", + "CVE.variants": "Cape Verdean Escudos,Cape Verdean Escudo,Escudos,Escudo,Esc", + "CZK.format": "%@ Kč", + "CZK.variants": "Czech Korunas,Czech Koruna,Kč", + "DJF.format": "%@ Fdj", + "DJF.variants": "Djibouti Francs,Djibouti Franc,Djiboutian Francs,Djiboutian Franc,Fdj", + "DKK.format": "%@ kr.", + "DKK.variants": "Danish Kroner,Danish Krone,kr.", + "DOP.format": "RD$ %@", + "DOP.variants": "Dominican Pesos,Dominican Peso,RD$", + "DZD.format": "%@ DA", + "DZD.variants": "Algerian Dinars,Algerian Dinar,دج", + "EGP.format": "£%@ EGP", + "EGP.variants": "Egyptian Pounds,Egyptian Pound", + "ERN.format": "%@ Nfk", + "ERN.variants": "Eritrean nakfa,Nakfa,Nfk", + "ETB.format": "%@ Br", + "ETB.variants": "Ethiopian Birr,Birr", + "EUR.format": "€ %@", + "EUR.variants": "Euro,€", + "FJD.format": "FJ$ %@", + "FJD.variants": "Fiji Dollars,Fiji Dollar,Fijian Dollars,Fijian Dollar,FJ$", + "FKP.format": "£ %@", + "FKP.variants": "Falkland Islands Pounds,Falkland Islands Pound,Falkland Pounds", + "GBP.format": "£ %@", + "GBP.variants": "Pound sterling,Pounds sterling,£,£,british pound,british pounds", + "GEL.format": "%@ lari", + "GEL.variants": "Lari,Georgian Lari", + "GHS.format": "GH₵ %@", + "GHS.variants": "Ghana Cedi,Cedi,GH₵", + "GIP.format": "%@ GIP", + "GIP.variants": "Gibraltar Pounds,Gibraltar Pound", + "GMD.format": "%@ D", + "GMD.variants": "Gambian Dalasi,Dalasi,Gambian Dalasis,Dalasis", + "GNF.format": "%@ FG", + "GNF.variants": "Guinean franc,Guinean francs", + "GTQ.format": "%@ Q", + "GTQ.variants": "Guatemalan Quetzales,Guatemalan quetzal,Quetzal,Quetzales", + "GYD.format": "G$ %@", + "GYD.variants": "Guyanese dollar,Guyanese dollars,G$", + "HKD.format": "HK$ %@", + "HKD.variants": "Hong Kong Dollar,Hong Kong Dollars,HK$", + "HNL.format": "L %@", + "HNL.variants": "Honduran Lempira,Honduran Lempiras,Lempira,Lempiras", + "HRK.format": "%@ Kn", + "HRK.variants": "Croatian Kuna,Kuna", + "HTG.format": "%@ G", + "HTG.variants": "Haitian Gourdes,Haitian Gourde,Gourdes,Gourde", + "HUF.format": "%@ Ft", + "HUF.variants": "Hungarian Forint,Forint", + "IDR.format": "Rp %@", + "IDR.variants": "Indonesian Rupiah,Rp", + "ILS.format": "%@ NIS", + "ILS.variants": "Israeli new shekel,new shekalim,new sheqalim,₪,NIS", + "INR.format": "INR %@", + "INR.variants": "Indian Rupee", + "IQD.format": "%@ IQD", + "IQD.variants": "Iraqi Dinars,Iraqi Dinar,ع.د", + "IRR.format": "%@ IR", + "IRR.variants": "Iranian Rial,Iranian Rials,IR", + "ISK.format": "%@ kr", + "ISK.variants": "Icelandic krona,Icelandic kronas,Iceland Krona,Iceland Kronas", + "JMD.format": "%@ JMD", + "JMD.variants": "Jamaican Dollar,Jamaican Dollars", + "JOD.format": "%@ JD", + "JOD.variants": "Jordanian Dinars,Jordanian Dinar,دينار,JD", + "JPY.format": "¥ %@", + "JPY.variants": "Japanese Yen,Yen,¥", + "KES.format": "%@ KSh", + "KES.variants": "Kenyan Shillings,Kenyan Shilling,KSh", + "KGS.format": "%@ sом", + "KGS.variants": "Kyrgyzstani Soms,Kyrgyzstani Som,Soms,Som,sом", + "KHR.format": "%@ R.", + "KHR.variants": "Cambodian Riel,Riel,៛,Cambodian Riels,Riels", + "KMF.format": "%@ FC", + "KMF.variants": "Comorian Francs,Comorian Franc,FC", + "KPW.format": "₩ %@", + "KPW.variants": "North Korean won", + "KRW.format": "₩ %@", + "KRW.variants": "South Korean Won,Won", + "KWD.format": "%@ KD", + "KWD.variants": "Kuwaiti Dinars,Kuwaiti Dinar,د.ك,KD", + "KYD.format": "$ %@", + "KYD.variants": "Cayman Islands Dollars,Cayman Islands Dollar", + "KZT.format": "%@ T", + "KZT.variants": "Kazakhstani Tenge,Tenge", + "LAK.format": "%@ K", + "LAK.variants": "Lao Kips,Lao Kip,Kips,Kip", + "LBP.format": "%@ .ل.ل", + "LBP.variants": "Lebanese Pound,Lebanese Pounds,.ل.ل", + "LKR.format": "Rs %@", + "LKR.variants": "Sri Lanka Rupee", + "LRD.format": "L$ %@", + "LRD.variants": "Liberian Dollars,Liberian Dollar,L$", + "LSL.format": "M %@", + "LSL.variants": "Lesotho maloti,Lesotho loti,loti,Maloti", + "LTL.format": "%@ Lt", + "LTL.variants": "Lithuanian Litas,Lithuanian Litai,Litas,Litai,Lt", + "LVL.format": "%@ Ls", + "LVL.variants": "Latvian Lats,Lat,Latvian Lati,Lati,Latvian Latu,Latu,Ls", + "LYD.format": "%@ LD", + "LYD.variants": "Libyan Dinar,Libyan Dinars,د.ل", + "MAD.format": "%@ Dh", + "MAD.variants": "Moroccan Dirham,Moroccan Dirhams,د.م.", + "MDL.format": "%@ lei", + "MDL.variants": "Moldovan Leu,Moldovan Lei,Leu", + "MGA.format": "Ar %@", + "MGA.variants": "Malagasy ariary,ariary", + "MKD.format": "%@ den.", + "MKD.variants": "Macedonian Denar,Macedonian Denari,den.", + "MMK.format": "K %@", + "MMK.variants": "Myanmar Kyat,Burmese kyat,kyat", + "MNT.format": "%@ ₮", + "MNT.variants": "Mongolian Tugrik,Tugrik,₮", + "MOP.format": "MOP$ %@", + "MOP.variants": "Macanese Pataca,Macao Pataca,Macau Pataca,Pataca,Macanese Patacas,Patacas,Macao Patacas,MOP$", + "MRO.format": "%@ UM", + "MRO.variants": "Mauritanian ouguiya,ouguiya", + "MUR.format": "Rs %@", + "MUR.variants": "Mauritius Rupee,Mauritius Rupees", + "MVR.format": "Rf %@", + "MVR.variants": "Maldivian Rufiyaa,Rufiyaa,Rf", + "MWK.format": "%@ MK", + "MWK.variants": "Malawian Kwacha,Kwacha", + "MXN.format": "$ %@", + "MXN.variants": "Mexican Peso,Mexican Pesos", + "MYR.format": "RM %@", + "MYR.variants": "Malaysian Ringgit,Ringgit", + "MZN.format": "%@ MT", + "MZN.variants": "Mozambican Metical,Mozambican Meticais,Metical,Meticais", + "NAD.format": "N$ %@", + "NAD.variants": "Namibian Dollar,Namibian Dollars,N$", + "NGN.format": "%@ ₦", + "NGN.variants": "Nigerian Naira,Naira,₦", + "NIO.format": "C$ %@", + "NIO.variants": "Nicaraguan Cordoba,Cordoba,Cordobas,Nicaraguan Cordobas,C$", + "NOK.format": "%@ kr", + "NOK.variants": "Norwegian Krone,Norwegian Kroner", + "NPR.format": "₨ %@", + "NPR.variants": "Nepalese Rupee,Nepalese Rupees,₨", + "NZD.format": "$%@ NZD", + "NZD.variants": "New Zealand Dollar,New Zealand Dollars", + "OMR.format": "%@ RO", + "OMR.variants": "Omani Rial,Omani Rials,ر.ع.,RO", + "PAB.format": "%@ B/.", + "PAB.variants": "Panamanian Balboa,Balboa,Panamanian Balboas,Balboas,B/.", + "PEN.format": "%@ S/.", + "PEN.variants": "Peruvian Nuevo Sol,Nuevo Sol,Peruvian Nuevo Soles,Nuevo Soles,S/.", + "PGK.format": "%@ K", + "PGK.variants": "Papua New Guinean Kina,Kina", + "PHP.format": "%@ ₱", + "PHP.variants": "Philippine Peso,Philippine Pesos,₱", + "PKR.format": "Rs %@", + "PKR.variants": "Pakistan Rupee,Pakistan Rupees", + "PLN.format": "%@ zł", + "PLN.variants": "Polish Zloty,Zloty,zł", + "PYG.format": "%@ ₲", + "PYG.variants": "Paraguayan Guarani,Guarani,Paraguayan Guaranies,Guaranies,₲", + "QAR.format": "%@ QR", + "QAR.variants": "Qatari Rial,Qatari Rials", + "RON.format": "%@ lei", + "RON.variants": "Romanian Lei,Romanian Leu", + "RSD.format": "%@ RSD", + "RSD.variants": "Serbian Dinar,Serbian Dinars", + "RUB.format": "%@ ₽", + "RUB.variants": "russian rouble,russian roubles,ru rouble,ru roubles,rus roubles,rus rouble,russian ruble,russian rubles,rouble,ruble,rubles,roubles,₽", + "RWF.format": "%@ RF", + "RWF.variants": "Rwandan franc,Rwandan francs", + "SAR.format": "%@ SR", + "SAR.variants": "Saudi Riyal,Saudi Riyals,﷼‎,ر.س", + "SBD.format": "SI$ %@", + "SBD.variants": "Solomon Islands Dollar,Solomon Islands Dollars,SI$", + "SCR.format": "%@ SR", + "SCR.variants": "Seychelles Rupees,Seychelles Rupee,Seychellois Rupees,Seychellois Rupee", + "SDG.format": "%@ £Sd", + "SDG.variants": "Sudanese Pound,Sudanese Pounds,ج.س.‏,£Sd", + "SEK.format": "%@ kr", + "SEK.variants": "Swedish Krona,Swedish Kronor", + "SGD.format": "S$ %@", + "SGD.variants": "Singapore Dollar,Singapore Dollars,S$", + "SHP.format": "£%@ SHP", + "SHP.variants": "St Helena Pounds,St Helena Pound,Saint Helena pound,Saint Helena pounds", + "SLL.format": "%@ Le", + "SLL.variants": "Sierra Leonean Leone,Sierra Leonean Leones,Leone,Leones", + "SOS.format": "%@ So.", + "SOS.variants": "Somali Shillings,Somali Shilling,Sh.,So.", + "SRD.format": "$ %@", + "SRD.variants": "Surinamese Dollar,Surinamese Dollars", + "SSP.format": "%@ SSP", + "SSP.variants": "South Sudanese pound,South Sudanese pounds", + "STD.format": "%@ Db", + "STD.variants": "São Tomé and Príncipe Dobra,São Tomé and Príncipe Dobras,Dobra,Dobras", + "SVC.format": "%@ ₡", + "SVC.variants": "El Salvador Colon,Salvadoran colon,Salvadoran colones", + "SYP.format": "S£ %@", + "SYP.variants": "Syrian Pound,Syrian Pounds,S£", + "SZL.format": "E %@", + "SZL.variants": "Swazi Lilangeni,Lilangeni,Swazi Emalangeni,Emalangeni", + "THB.format": "฿ %@", + "THB.variants": "Thai Baht,Baht,฿", + "TJS.format": "%@ TJS", + "TJS.variants": "Tajikistani Somoni,Somoni", + "TMT.format": "%@ TMT", + "TMT.variants": "Turkmenistan Manat,Manat", + "TND.format": "%@ TD", + "TND.variants": "Tunisian Dinar,Tunisian Dinars,د.ت,TD", + "TOP.format": "T$ %@", + "TOP.variants": "Tongan Paʻanga,Paʻanga,T$", + "TRY.format": "₺%@", + "TRY.variants": "Turkish Lira,₺", + "TTD.format": "TT$ %@", + "TTD.variants": "Trinidad and Tobago Dollar,Trinidad and Tobago Dollars,TT$", + "TWD.format": "NT$ %@", + "TWD.variants": "New Taiwan Dollars,New Taiwan Dollar,NT$,NT Dollar, NTD", + "TZS.format": "%@ TSh", + "TZS.variants": "Tanzanian Shilling,Tanzanian Shillings,Tanzanian Shillingi,TSh", + "UAH.format": "%@ UAH", + "UAH.variants": "Ukrainian Hryvnia,Ukrainian Hryvni,Ukrainian Hryven',Hryvnia,Hryvni,Hryven',₴,grn.", + "UGX.format": "%@ USh", + "UGX.variants": "Ugandan Shilling,Ugandan Shillings,USh", + "USD.format": "$%@", + "USD.variants": "dollar USA,dollars USA,dollar,USD,dollars,U.S. Dollars,U.S. Dollar,USA Dollars,USA Dollar,$", + "UYU.format": "$U %@", + "UYU.variants": "Uruguayan Peso,Uruguayan Pesos,$U", + "UZS.format": "%@ som", + "UZS.variants": "Uzbekistani som,som,so'm", + "VEF.format": "Bs.F. %@", + "VEF.variants": "Venezuelan Bolívar,Venezuelan Bolivar,Venezuelan Bolívares,Venezuelan Bolivares,bolívar fuerte,bolivar fuerte,bolívares fuertes,Bs.F.", + "VND.format": "%@ ₫", + "VND.variants": "Vietnamese Dong,Dong,₫", + "VUV.format": "%@ VT", + "VUV.variants": "Vanuatu Vatu,Vatu", + "WST.format": "WS$ %@", + "WST.variants": "Samoan Tala,Tala,WS$", + "XAF.format": "%@ FCFA", + "XAF.variants": "CFA Franc (Beac),Central African CFA franc,Central African CFA francs,CFA francs,CFA franc,Franc CFA BEAC,FCFA", + "XCD.format": "EC$ %@", + "XCD.variants": "East Caribbean Dollar,East Caribbean Dollars,EC$", + "XOF.format": "%@ CFA", + "XOF.variants": "West African CFA franc,franc CFA BCEAO,CFA", + "XPF.format": "%@ F", + "XPF.variants": "CFP Franc,CFP Francs,franc pacifique,francs pacifique", + "YER.format": "%@ YER", + "YER.variants": "Yemeni Rial,Yemeni Rials,﷼", + "ZAR.format": "R %@", + "ZAR.variants": "South African Rand,Rand", + "ZMW.format": "%@ ZMK", + "ZMW.variants": "Zambian Kwacha,Kwacha,ZMK" + }, + "Data": { + "bit.format": "b", + "bit.symbol": "b", + "bit.variants": "bit,bits,b", + "byte.format": "B", + "byte.symbol": "B", + "byte.variants": "byte,bytes,B" + }, + "Dates": { + "current_time.variants": "time,now", + "local_time.variants": "local,local timezone,local time zone", + "today.variants": "today", + "tomorrow.variants": "tomorrow", + "yesterday.variants": "yesterday" + }, + "IECPrefixes": { + "exbi.prefix": "exbi", + "exbi.symbol": "Ei", + "gibi.prefix": "gibi", + "gibi.symbol": "Gi", + "kibi.prefix": "kibi", + "kibi.symbol": "Ki", + "mebi.prefix": "mebi", + "mebi.symbol": "Mi", + "pebi.prefix": "pebi", + "pebi.symbol": "Pi", + "tebi.prefix": "tebi", + "tebi.symbol": "Ti", + "yobi.prefix": "yobi", + "yobi.symbol": "Yi", + "zebi.prefix": "zebi", + "zebi.symbol": "Zi" + }, + "Length": { + "cable.format": "cable", + "cable.variants": "cable,cables", + "chain.format": "chain", + "chain.variants": "chain,chains", + "foot.format": "ft", + "foot.variants": "foot,foots,ft,ft.,feet,feets", + "furlong.format": "furlong", + "furlong.variants": "furlong,furlongs", + "inch.format": "″", + "inch.variants": "inches,inch,″", + "league.format": "league", + "league.variants": "league,leagues", + "meter.format": "m", + "meter.symbol": "m", + "meter.variants": "meter,metre,metres,meters,m", + "mile.format": "mi.", + "mile.variants": "mile,miles,mi,mi.", + "nautical_mile.format": "n.m.", + "nautical_mile.variants": "nmi,nautical mile,nautical miles,n.m.", + "point.format": "pt", + "point.variants": "point,points,pt,pt.", + "yard.format": "yd", + "yard.variants": "yard,yards,yd,yd." + }, + "Numbers": { + "binary.variants": "binary,bin", + "decimal.variants": "decimal,dec", + "hex.variants": "hex", + "octal.variants": "octal", + "scientific.variants": "scientific, sci, exp, exponent, exponential" + }, + "Operations": { + "assignment.variants": "equal,is", + "avg.variants": "average,avg", + "cbrt.variants": "cbrt,cubic root,cube root,cubed root", + "conversion.variants": "in,into,as,to", + "divide.variants": "divide,divide by,divided by", + "minus.variants": "minus,without,subtract", + "multiply.variants": "times,multiplied by,multiply,mul,mult", + "plus.variants": "with,plus,and", + "prev.variants": "prev", + "sqrt.variants": "sqrt,square root", + "sum.variants": "sum,total" + }, + "Percent": { + "as_a_percent_of.variants": "as a % of,as a percent of", + "as_a_percent_off.variants": "as a % off,as a percent off,as a % decrease of,as a percent decrease of", + "as_a_percent_on.variants": "as a % on,as a percent on,as a % increase of,as a percent increase of", + "percent.format": "%%", + "percent.variants": "%,percent,percents,pct,pct.", + "percent_of.variants": "from,of", + "percent_of_what_is.variants": "of what is", + "percent_off.variants": "off", + "percent_off_what_is.variants": "off what is", + "percent_on.variants": "on", + "percent_on_what_is.variants": "on what is" + }, + "SIPrefixes": { + "atto.prefix": "atto", + "atto.symbol": "a", + "centi.prefix": "centi", + "centi.symbol": "c", + "deca.prefix": "deca", + "deca.symbol": "da", + "deci.prefix": "deci", + "deci.symbol": "d", + "exa.prefix": "exa", + "exa.symbol": "E", + "femto.prefix": "femto", + "femto.symbol": "f", + "giga.prefix": "giga", + "giga.symbol": "G", + "hecto.prefix": "hecto", + "hecto.symbol": "h", + "kilo.prefix": "kilo", + "kilo.symbol": "k", + "mega.prefix": "mega", + "mega.symbol": "M", + "micro.prefix": "micro", + "micro.symbol": "µ", + "milli.prefix": "milli", + "milli.symbol": "m", + "nano.prefix": "nano", + "nano.symbol": "n", + "peta.prefix": "peta", + "peta.symbol": "P", + "pico.prefix": "pico", + "pico.symbol": "p", + "tera.prefix": "tera", + "tera.symbol": "T", + "yocto.prefix": "yocto", + "yocto.symbol": "y", + "yotta.prefix": "yotta", + "yotta.symbol": "Y", + "zepto.prefix": "zepto", + "zepto.symbol": "z", + "zetta.prefix": "zetta", + "zetta.symbol": "Z" + }, + "Samples": { + "sample.welcome": "# Summarum\n8 / (45 - 20%)\n5 inches in cm\n$9 in euro\n100 km/h in mph" + }, + "Scales": { + "billion.variants": "billion,Billion,milliard,Milliard,billions,Billions,milliards,Milliards", + "million.variants": "million,Million,millions,Millions,M", + "thousand.variants": "thousand,Thousand,thousands,Thousands,th.,th,k" + }, + "Temperature": { + "celsius.format": "°C", + "celsius.variants": "celsius,C,°C", + "fahrenheit.format": "°F", + "fahrenheit.variants": "fahrenheit,fahrenheits,F,°F", + "kelvin.format": "K", + "kelvin.variants": "kelvin,kelvins,K" + }, + "Time": { + "day.format": "day", + "day.variants": "day,days,d", + "hour.format": "h", + "hour.variants": "h,hr,hour,hours", + "minute.format": "min", + "minute.variants": "min,minute,minutes", + "month.format": "mon.", + "month.variants": "month,monthes,months,mon,mon.", + "second.format": "s", + "second.symbol": "s", + "second.variants": "s,sec,second,seconds", + "week.format": "w", + "week.variants": "week,w,weeks", + "year.format": "yr", + "year.variants": "y,yr,year,years" + }, + "Volume": { + "cbcm.variants": "cc,ccm", + "cbinch.variants": "c.i.,CI,cui,cu inch", + "cup.format": "cup", + "cup.variants": "cup,cups", + "gallon.format": "gal.", + "gallon.variants": "gallon,gallons,gal.,gal", + "litre.format": "L", + "litre.symbol": "l", + "litre.variants": "litre,litres,liter,liters,l,L", + "pint.format": "pint", + "pint.variants": "pint,pints", + "quart.format": "qt.", + "quart.variants": "quart,quarts,qt.,qt", + "tablespoon.format": "tbsp.", + "tablespoon.variants": "tablespoon,tablespoons,table spoon,table spoons,tbsp.,tbsp,T.", + "teaspoon.format": "tsp.", + "teaspoon.variants": "teaspoon,teaspoons,tea spoon,tea spoons,t.,tsp.,tsp", + "volume.format": "%@³", + "volume.templates": "cu %@,cubic %@,cb %@,cb%@,%@³,%@ ³,%@ 3" + }, + "Weight": { + "carat.format": "ct", + "carat.variants": "carat,carats,CD,ct", + "centner.format": "centner", + "centner.variants": "centner,quintal,centners,quintals", + "gram.format": "g", + "gram.symbol": "g", + "gram.variants": "gram,gramme,grams,grammes,g", + "ounce.format": "oz", + "ounce.variants": "ounce,ounces,oz", + "pound.format": "lb", + "pound.variants": "pound,pounds,lb,lbm", + "stone.format": "st", + "stone.variants": "stone,stones,st", + "tonne.format": "t", + "tonne.symbol": "t", + "tonne.variants": "tonne,tonnes,t,metric ton" + } +}; + +export const RU: VocabLocale = { + "Angle": { + "degree.format": "°,градус,градусов,градусы", + "degree.variants": "°,градус,градусов,градусах,градуса,град,град.", + "radian.format": "рад", + "radian.variants": "радиан,рад,радианах" + }, + "Area": { + "acre.format": "акр", + "acre.variants": "акр,акров,акра,акрах", + "are.format": "а", + "are.variants": "ар,ара,арах,аров", + "hectare.format": "га", + "hectare.variants": "гектар,гектаров,гектара,гектарах,га", + "ping.format": "пинг", + "ping.variants": "пинг,пинги" + }, + "Css": { + "pixel.variants": "пиксел,пиксела,пикселу,пикселом,пикселе,пиксель,пикселя,пикселю,пекселем,пиксели,пикселей,пикселям,пикселями,пикселях,пикс." + }, + "Currency": { + "AED.variants": "Дирхам ОАЭ,Дирхамы ОАЭ,Дирхам", + "AFN.variants": "Афгани", + "ALL.format": "%@ Lek", + "ALL.variants": "Лек,Албанский лек", + "AMD.format": "%@ Драм", + "AMD.variants": "Драм,Драмов,Драмы,Армянский драм,Армянские драмы,Армянских драмов", + "ANG.format": "%@ ƒ", + "ANG.variants": "Гульден,Гульденов,Гульдены,Гульденах", + "AOA.variants": "Кванза,Кванзы,Ангольская кванза,Ангольские кванзы,кванзах", + "ARS.variants": "Аргентинское песо,Аргентинских песо", + "AUD.variants": "Австралийский доллар,Австралийских долларов,Австралийских долларах", + "AWG.format": "%@ ƒ", + "AWG.variants": "Арубанский флорин,Арубанских флоринов,Арубанских флоринах", + "AZN.format": "%@ ман.", + "AZN.variants": "Азербайджанский манат,Азербайджанских манат,Азербайджанских манатах,манат,манатах,man.,ман.", + "BAM.variants": "Конвертируемая марка,Конвертируемых марок,Конвертируемых марках", + "BBD.variants": "Барбадосский доллар,Барбадосские доллары,Барбадосских долларов,Барбадосских долларах", + "BDT.variants": "Така,Таков,Таках,Таки", + "BGN.format": "%@ лв", + "BGN.variants": "Болгарский лев,Болгарских левов,Болгарские левы,Болгарских левах,Лев,Левов,Левах,Болг. левов,Болг. лев,Болг. левах", + "BHD.variants": "Бахрейнский динар,Бахрейнских динаров,Бахрейнских динарах,Динар,Динарах,Динаров", + "BIF.variants": "Бурундийский франк,Бурундийских франков,Бурундийские франки,Бурундийских франках", + "BMD.format": "%@ $", + "BMD.variants": "Бермудский доллар,Бермудских долларов,Бермудских долларах,Бермудские доллары", + "BND.format": "%@ B$", + "BND.variants": "Брунейский доллар,Брунейских долларов,Брунейских долларах,Брунейские доллары", + "BOB.format": "%@ Bs", + "BOB.variants": "Боливиано", + "BRL.format": "%@ R$", + "BRL.variants": "Бразильский реал,Бразильских реалов,Бразильских реалах,Бразильские реалы,Реал,Реалов,Реалах", + "BSD.format": "%@ B$", + "BSD.variants": "Багамский доллар,Багамских долларов,Багамских долларах,Багамские доллары", + "BTC.variants": "Bitcoins,Bitcoin,Биткоин,Биткойн,Биткоинов,Биткойнов,Биткоинах,Биткойнах,Биткоины,Биткойны", + "BTN.format": "%@ Nu", + "BTN.variants": "Нгултрум,Нгултрумах,Нгултрумов", + "BWP.variants": "Пула,Пул,Пулах", + "BYN.format": "%@ BYN", + "BYN.variants": "белорусский рубль,белорусских рублей,белорусских рублях,белорусских рубля,бел. рублях,бел. рубль,бел. рублей,бел. рубля,бел руб,бел руб.,бел. руб.,Br", + "BZD.format": "%@ BZ$", + "BZD.variants": "Белизский доллар,Белизских долларах,Белизских долларов,Белизские доллары", + "CAD.format": "%@ CA$", + "CAD.variants": "Канадский доллар,Канадских долларов,Канадских долларах,Канадские доллары", + "CDF.variants": "Конголезский франк,Конголезских франков,Конголезских франках,Конголезские франки", + "CHF.format": "%@ SFr", + "CHF.variants": "Швейцарский франк,Швейцарских франков,Швейцарских франках,Швейцарские франки,Франк,Франков,Франках", + "CLP.variants": "Чилийское песо,Чилийских песо", + "CNY.variants": "Юань,Юаней,Юаня,Юанях", + "COP.variants": "Колумбийское песо,Колумбийские песо,Колумбийских песо", + "CRC.format": "%@ C", + "CRC.variants": "Костариканский колон,Костариканских колонах,Костариканских колон,Колон,Колонах", + "CUP.format": "%@ $MN", + "CUP.variants": "Кубинское песо,Кубинский песо,Кубинских песо", + "CVE.variants": "Эскудо Кабо-Верде,Эскудо", + "CZK.variants": "Чешская крона,Чешские кроны,Чешских крон,Чешских кронах", + "DJF.variants": "Франк Джибути,Франков Джибути,Франках Джибути", + "DKK.format": "%@ kr", + "DKK.variants": "Датские кроны,Датская Крона,Датских кронах,Датских крон", + "DOP.format": "%@ RD$", + "DOP.variants": "Доминиканское песо,Золотой песо,Доминиканских песо", + "DZD.variants": "Алжирские динары,Алжирский динар,Алжирских динар,Алжирских динарах", + "EGP.format": "%@ EGP", + "EGP.variants": "Египетские фунты,Египетский Фунт,Египетских фунтов,Египетских Фунтах", + "ERN.variants": "Накфа,Накфах,Накф", + "ETB.variants": "Эфиопский быр,Быр,Быров,Бырах", + "EUR.format": "%@ €", + "EUR.variants": "Евро", + "FJD.format": "%@ FJ$", + "FJD.variants": "Доллар Фиджи,Долларах Фиджи,Долларов Фиджи", + "FKP.format": "%@ £", + "FKP.variants": "Фунт Фолклендских островов,Фолклендский фунт,Фолклендских фунтов,Фолклендских фунтах", + "GBP.format": "%@ £", + "GBP.variants": "Фунт стерлингов,Фунтов стерлингов,Фунтах стерлингов,Британский фунт,Британских фунтов,Фунт,Фунтов,Британских фунтах,Фунтах", + "GEL.variants": "Грузинский лари,Лари", + "GHS.format": "%@ GH₵", + "GHS.variants": "Ганский седи,Седи", + "GIP.variants": "Гибралтарский фунт,Гибралтарских фунтов,Гибралтарских фунтах", + "GMD.variants": "Даласи,Гамбийский даласи", + "GNF.format": "%@ GFr", + "GNF.variants": "Гвинейский франк,Гвинейских франках,Гвинейских франков", + "GTQ.variants": "Гватемальский кетсаль,Кетсаль,Кетцаль,Кетсалей,Кетсалях", + "GYD.format": "%@ GY$", + "GYD.variants": "Гайанские доллары,Гайанский доллар,Гайанских долларах,Гайанских долларов", + "HKD.format": "%@ HK$", + "HKD.variants": "Гонконгские доллары,Гонконгский доллар,Гонконгских долларов,Гонконгских долларах", + "HNL.format": "%@ L", + "HNL.variants": "Гондурасская лемпира,Лемпира,Лемпирах,Лемпир", + "HRK.variants": "Хорватская куна,Хорватских кунах,Куна,Кунах", + "HTG.variants": "Гурд,Гаитянский гурд,Гурдов,Гурдах", + "HUF.format": "%@ ft", + "HUF.variants": "Форинт,Форинтов,Форинтах,Венгерский форинт", + "IDR.format": "%@ Rp", + "IDR.variants": "Индонезийские рупии,Индонезийская рупия,Индонезийских рупиях,Индонезийских рупий", + "ILS.variants": "Новый израильский шекель,Новый шекель,Новых шекелей,новых шекелях", + "INR.format": "%@ Re.", + "INR.variants": "Индийские рупии,Индийская рупия,Индийских рупиях,Индийских рупий", + "IQD.format": "%@ ID", + "IQD.variants": "Иракские Динары,Иракский Динар,Иракских Динарах,Иракских Динаров", + "IRR.variants": "Иранские риалы,Иранский риал,Иранских риалов,Иранских риалах,Риал,Риалах,Риалов", + "ISK.variants": "Исландские кроны,Исландская крона,Исландских крон,Исландских кронах", + "JMD.variants": "Ямайские доллары,Ямайский доллар,Ямайских долларов,Ямайских долларах", + "JOD.variants": "Иорданские динары,Иорданский динар,Иорданских динаров,Иорданских динарах", + "JPY.format": "%@ ¥", + "JPY.variants": "японская иена,иена,иен,иены,йен,йены,йена,японская йена", + "KES.variants": "Кенийские шиллинги,Кенийский шиллинг,Кенийских шиллингов,Кенийских шиллингах", + "KGS.format": "%@ сом", + "KGS.variants": "Кыргызский сом,Сом,Сомов,Сомах,Сома", + "KHR.variants": "Камбоджийский Риель,Риель,Риелей,Риелях", + "KMF.variants": "Франк Комор,Коморский франк,Коморских франках", + "KPW.format": "%@ ₩", + "KPW.variants": "Северокорейская вона,Вона КНДР,Северокорейских вон,Северокорейских вонах", + "KRW.format": "%@ ₩", + "KRW.variants": "Южнокорейская вона,Вона,вон,вонах,Южнокорейских вонах", + "KWD.variants": "Кувейтский динар,Кувейтских динарах,Кувейтских динаров", + "KYD.format": "%@ $", + "KYD.variants": "Доллар Островов Кайман,Доллар Каймановых островов,Долларах Каймановых островов", + "KZT.variants": "Казахстанский Тенге,Тенге", + "LAK.variants": "Лаосский кип,Кип,Кипах", + "LBP.variants": "Ливанские Фунты,Ливанский Фунт,Ливанских фунтах", + "LKR.format": "%@ Rs", + "LKR.variants": "Шри-Ланкийская рупия,Шри-Ланкийских рупий,Шри-Ланкийских рупиях", + "LRD.format": "%@ L$", + "LRD.variants": "Либерийские Доллары,Либерийский доллар,Либерийских долларах", + "LSL.format": "%@ M", + "LSL.variants": "Лоти,Лоти Лесото,Малоти", + "LTL.variants": "Литовский лит,Лит,Литах", + "LVL.variants": "Латвийский лат,Лат,Латах", + "LYD.variants": "Ливийские динары,Ливийский динар,Ливийских динарах,Ливийских динар", + "MAD.variants": "Марокканский дирхам,Дирхам,Дирхамов,Дирхамах", + "MDL.variants": "Молдавский лей,Молдавских леев,Молдавских леях,Лей,молд. лей,молд. леях,молд. леев", + "MGA.format": "%@ Ar.", + "MGA.variants": "Малагасийский ариари,Ариари", + "MKD.format": "%@ ден.", + "MKD.variants": "Македонский денар,Денар,Денарах", + "MMK.format": "%@ K", + "MMK.variants": "Кьят,Кьятах", + "MNT.variants": "Монгольский тугрик,Тугрик,Тугриков,Тугриках", + "MOP.format": "%@ $", + "MOP.variants": "Патака Макао,Патака,Патак,Патаках", + "MRO.variants": "Мавританская угия,Угия,Угий,Угиях", + "MUR.format": "%@ Rs", + "MUR.variants": "Маврикийская рупия,Маврикийских рупий,Маврикийских рупиях", + "MVR.format": "%@ Rf", + "MVR.variants": "Мальдивская руфия,Руфия,Руфий,Руфиях", + "MWK.variants": "Малавийская квача,Малавийских квач,Малавийских квачах,Квача,Квачах", + "MXN.format": "%@ $", + "MXN.variants": "Мексиканских песо,Мексиканское песо", + "MYR.format": "%@ RM", + "MYR.variants": "Малайзийский ринггит,Ринггит,Ринггитах", + "MZN.variants": "Мозамбикский метикал,Метикал,Метикалов,Метикалах", + "NAD.format": "%@ N$", + "NAD.variants": "Доллар Намибии,Намибийский доллар,Намибийских долларов,Намибийских долларах", + "NGN.variants": "Найра,Нигерийская найра,Найр,Найрах", + "NIO.format": "%@ C$", + "NIO.variants": "Никарагуанская кордоба,Золотая Кордоба,Кордоба,Кордоб,Кордобах", + "NOK.variants": "Крона,Норвежская крона,Норвежских крон,Норвежских кронах,Крон,Кронах", + "NPR.format": "%@ ₨", + "NPR.variants": "Непальские рупии,Непальская рупия,Непальских рупий,Непальских рупиях", + "NZD.variants": "Новозеландские доллары,Новозеландский доллар,Новозеландских долларах,Новозеландских долларов", + "OMR.variants": "Оманские риалы,Оманский риал,Оманских риалов,Оманских риала", + "PAB.format": "%@ B", + "PAB.variants": "Панамский бальбоа,Бальбоа", + "PEN.variants": "Новый соль,Перуанский Новый соль,Солей,Новых солей,Новых солях,Солях", + "PGK.variants": "Кина", + "PHP.format": "%@ PhP", + "PHP.variants": "Филиппинское песо,Филиппинских песо", + "PKR.format": "%@ Rs", + "PKR.variants": "Пакистанских рупий,Пакистанских рупиях,Пакистанская рупия", + "PLN.variants": "Польский злотый,Злотый,Злотых,Злотые", + "PYG.format": "%@ G", + "PYG.variants": "Парагвайский гуарани,Гуарани", + "QAR.variants": "Катарские риалы,Катарский риал,Катарских риалах", + "RON.variants": "Румынский лей,Новый румынский лей,Лей,Леев,Леях,Румынских леев,Румынских леях", + "RSD.format": "%@ дин.", + "RSD.variants": "Сербские динары,Сербский динар,Сербских динарах,Сербских динар", + "RUB.variants": "Российский рубль,Российских рублей,Российских рублях,Российские рубли,Рос. рубль,Рос. рублей,Рос. рублях,Рос. руб.,росс. руб.,росс руб,рос руб.,рубль,руб,руб.,₽,рубля,рублю,рублем,рубле,рубли,рублей,рублями,рублях", + "RWF.variants": "Франк Руанды,Руандийский франк,Руандийских франков,Руандийских франках", + "SAR.variants": "Саудовский риял,Риял Саудовской аравии,Риял,Риялов,Риялах,Саудовских риялах", + "SBD.format": "%@ SI$", + "SBD.variants": "Доллар Соломоновых Островов,Долларов Соломоновых Островов,Долларах Соломоновых Островов", + "SCR.variants": "Сейшельские рупии,Сейшельская рупия,Сейшельских рупиях,Сейшельских рупий", + "SDG.format": "%@ £", + "SDG.variants": "Суданские фунты,Суданский фунт,Суданских фунтов,Суданских фунтах", + "SEK.variants": "Шведские кроны,Шведская крона,Шведских крон,Шведских кронах", + "SGD.format": "%@ S$", + "SGD.variants": "Сингапурские доллары,Сингапурский доллар,Сингапурских долларов,Сингапурских долларах", + "SHP.format": "%@ £", + "SHP.variants": "Фунт Святой Елены,Фунтов Святой Елены,Фунтах Святой Елены", + "SLL.variants": "Леоне", + "SOS.variants": "Сомалийские шиллингы,Сомалийский шиллинг,Шиллинг,Шиллингов,Шиллингах,Сомалийских шиллингах", + "SRD.format": "%@ $", + "SRD.variants": "Суринамский доллар,Суринамских долларов,Суринамских долларах", + "SSP.variants": "Южносуданский фунт,Южно-суданский фунт,Южносуданских фунтов,Южносуданских фунтах", + "STD.variants": "Добра,Добр,Добрах", + "SVC.format": "%@ C", + "SVC.variants": "Сальвадорский колон,Колонов,Колон,Колонах", + "SYP.format": "%@ S£", + "SYP.variants": "Сирийские фунты,Сирийский фунт,Сирийских фунтов,Сирийских фунтах", + "SZL.format": "%@ E", + "SZL.variants": "Свазилендский лилангени,Эмалангени,Лилангени", + "THB.format": "%@ B", + "THB.variants": "Тайский бат,Бат,Батов,Батах", + "TJS.format": "%@ смн.", + "TJS.variants": "Таджикский сомони,Сомони", + "TMT.variants": "Туркменский манат,Новый туркменский манат,Манат,Манатов,Туркменских манатов,Маната,Туркменских маната", + "TND.variants": "Тунисские Динары,Тунисский Динар,Тунисских Динаров,Тунисских Динарах", + "TOP.format": "%@ T$", + "TOP.variants": "Тонганская паанга,Паанга,Паангах", + "TRY.variants": "Турецкая лира,Турецких лир,Турецких лирах,Турецкие лиры", + "TTD.format": "%@ TT$", + "TTD.variants": "Доллар Тринидада и Тобаго,Доллар,Долларов Тринидада и Тобаго,Долларах Тринидада и Тобаго", + "TWD.format": "%@ NT$", + "TWD.variants": "Новые тайваньские доллары,Новый тайваньский доллар,Новых тайваньских долларах,Новых тайваньских долларов", + "TZS.variants": "Танзанийские шиллинги,Танзанийский шиллинг,Танзанийских шиллингов,Танзанийских шиллингах", + "UAH.format": "%@ грн.", + "UAH.variants": "Украинские гривны,Украинская гривна,Гривна,Гривнах,Гривен,грн.", + "UGX.variants": "Угандийские шиллинги,Угандийский шиллинг,Угандийских шиллингов,Угандийских шиллингах", + "USD.format": "%@ $", + "USD.variants": "Доллары США,Доллар США,Доллар Соединенных Штатов Америки,USD,ам. доллар,амер. доллар,долларов сша,долларах сша,доллары сша,доллар,доллара,долларом,доллары,долларов,долларами,долларах,$", + "UYU.format": "%@ $U", + "UYU.variants": "Уругвайское песо,Уругвайских песо", + "UZS.format": "%@ so’m", + "UZS.variants": "Узбекский сум,Сум,Сумов,Сумах,Узбекских сумах,Узбекских сумов", + "VEF.format": "%@ BsF", + "VEF.variants": "Венесуэльские боливары,Венесуэльский Боливар,Боливар фуэрте,Венесуэльских боливаров,Венесуэльских боливарах,Боливарах фуэрте", + "VND.format": "%@ dd", + "VND.variants": "Вьетнамский донг,Донг,Донгов,Донгах", + "VUV.format": "%@ Vt", + "VUV.variants": "Вануатский вату,Вату", + "WST.format": "%@ WS$", + "WST.variants": "Самоанская тала,Тала,Тал,Талах", + "XAF.format": "%@ BEAC", + "XAF.variants": "Франк КФА BEAC,Франк КФА,Франков КФА,Франках КФА", + "XCD.format": "%@ EC$", + "XCD.variants": "Восточно-карибские доллары,Восточно-карибский доллар,Восточно-карибских долларов,Восточно-карибских долларах", + "XOF.format": "%@ BCEAO", + "XOF.variants": "Франк КФА BCEAO,Франк Африканского финансового сообщества,BCEAO", + "XPF.variants": "Французский тихоокеанский франк,Франк КФП,Франков КФП,Франках КФП", + "YER.format": "%@ YR", + "YER.variants": "Йеменские риалы,Йеменский риал,Йеменских риалов,Йеменских риалах", + "ZAR.format": "%@ R", + "ZAR.variants": "Южноафриканский рэнд,Южноафриканский ранд,Ранд,Рэнд,Рэндов,Рэндах,рандов,Рандах", + "ZMW.format": "%@ K", + "ZMW.variants": "Замбийская квача,Квача,Квач,Квачах" + }, + "Data": { + "bit.format": "бит", + "bit.symbol": "бит", + "bit.variants": "бит,битов,битах,битам,биты", + "byte.format": "Б", + "byte.symbol": "Б", + "byte.variants": "байт,байтов,байтам,байтах,Б,байты" + }, + "Dates": { + "current_time.variants": "время,сейчас,time,now", + "local_time.variants": "local,local timezone,local time zone,местное время,локальное время,локально", + "today.variants": "сегодня", + "tomorrow.variants": "завтра", + "yesterday.variants": "вчера" + }, + "IECPrefixes": { + "exbi.prefix": "эксби", + "exbi.symbol": "Эи", + "gibi.prefix": "гиби", + "gibi.symbol": "Ги", + "kibi.prefix": "киби", + "kibi.symbol": "Ки", + "mebi.prefix": "меби", + "mebi.symbol": "Ми", + "pebi.prefix": "пеби", + "pebi.symbol": "Пи", + "tebi.prefix": "теби", + "tebi.symbol": "Ти", + "yobi.prefix": "йоби", + "yobi.symbol": "Йи", + "zebi.prefix": "зеби", + "zebi.symbol": "Зи" + }, + "Length": { + "cable.format": "кабельт", + "cable.variants": "кабельт,кабельтов,кабельтах,кабельта", + "chain.format": "чейн", + "chain.variants": "чейн,чейнов,чейнах,чейна", + "foot.format": "фут", + "foot.variants": "фут,футов,футах,фута", + "furlong.format": "фарлонг", + "furlong.variants": "фарлонг,фарлонгов,фарлонгах,фарлонга", + "inch.format": "дюйм", + "inch.variants": "дюйм,дюймов,дюймах,дюйма", + "league.format": "лига", + "league.variants": "лига,лиги,лигах", + "meter.format": "м", + "meter.symbol": "м", + "meter.variants": "метров,метрах,метр,метра,м", + "mile.format": "миля", + "mile.variants": "миля,миль,милях,мили", + "nautical_mile.format": "морская миля", + "nautical_mile.variants": "морская миля,морских миль,морских милях,морских мили", + "point.format": "точка", + "point.variants": "точка,точек,точках,точки", + "yard.format": "ярд", + "yard.variants": "ярд,ярдов,ярдах,ярда" + }, + "Operations": { + "assignment.variants": "равно,соответствует", + "avg.variants": "среднее,средн.,средн", + "cbrt.variants": "кубический корень,куб корень,куб. корень", + "conversion.variants": "в,как", + "divide.variants": "разделить,разделить на,делим,делим на", + "minus.variants": "минус,без", + "multiply.variants": "раз по,раз,умножить,по", + "plus.variants": "и,плюс,вместе с", + "prev.variants": "пред.,пред", + "sqrt.variants": "квадратный корень,кв корень,кв. корень", + "sum.variants": "всего,сумма,итог,итого" + }, + "Percent": { + "as_a_percent_of.variants": "как процент от,как процент", + "as_a_percent_off.variants": "как процентное уменьшение от,как процентное уменьшение для,как процентное уменьшение из", + "as_a_percent_on.variants": "как процентная добавка к,как процентное увеличение для,как процентное увеличение к", + "percent.variants": "процент,процента,проценту,процентом,проценте,проценты,процентов,процентам,процентами,процентах,проц,проц.", + "percent_of.variants": "от,из", + "percent_of_what_is.variants": "чего является,чего есть,от чего является,от чего есть,от чего,чего", + "percent_off.variants": "скидка от,снятие из,снятие от,снятие ото,уменьшение из,уменьшение для,уменьшение к", + "percent_off_what_is.variants": "уменьшение от чего,снятие от чего,уменьшение чего,снятие чего", + "percent_on.variants": "добавка к,прибавка к,увеличение к,увеличение для", + "percent_on_what_is.variants": "увеличение чего,увеличение от чего,добавление к чему" + }, + "SIPrefixes": { + "atto.prefix": "атто", + "atto.symbol": "а", + "centi.prefix": "санти", + "centi.symbol": "с", + "deca.prefix": "дека", + "deca.symbol": "да", + "deci.prefix": "деци", + "deci.symbol": "д", + "exa.prefix": "экса", + "exa.symbol": "Э", + "femto.prefix": "фемто", + "femto.symbol": "ф", + "giga.prefix": "гига", + "giga.symbol": "Г", + "hecto.prefix": "гекто", + "hecto.symbol": "ч", + "kilo.prefix": "кило", + "kilo.symbol": "к", + "mega.prefix": "мега", + "mega.symbol": "М", + "micro.prefix": "микро", + "micro.symbol": "мк", + "milli.prefix": "милли", + "milli.symbol": "м", + "nano.prefix": "нано", + "nano.symbol": "н", + "peta.prefix": "пета", + "peta.symbol": "П", + "pico.prefix": "пико", + "pico.symbol": "п", + "tera.prefix": "тера", + "tera.symbol": "Т", + "yocto.prefix": "иокто", + "yocto.symbol": "и", + "yotta.prefix": "иотта", + "yotta.symbol": "И", + "zepto.prefix": "зепто", + "zepto.symbol": "з", + "zetta.prefix": "зетта", + "zetta.symbol": "З" + }, + "Samples": { + "sample.welcome": "# Summarum\n8 / (45 - 20%)\n5 дюймов в см\n$9 в евро\n100 км/ч в милях" + }, + "Scales": { + "billion.variants": "миллиард,миллиардов,миллиарда,млрд,млрд.", + "million.variants": "миллион,мил.,мил,милл.,милл,миллиона,миллионов,М,млн,млн.", + "thousand.variants": "тысяч,тысячь,тысячи,тысяча,тыс.,тыс,т,т." + }, + "Temperature": { + "celsius.variants": "цельсий,цельсия,цельсиев,цельсиях,цельсиями,градусов цельсия", + "fahrenheit.variants": "градус фаренгейта,градуса фаренгейта,градусу фаренгейта,градусом фаренгейта,градусы фаренгейта,градусов фаренгейта,градусам фаренгейта,градусами фаренгейта,градусах фаренгейта", + "kelvin.variants": "кельвин,градус кельвина,градуса кельвина,градусов кельвина,кельвинов,кельвина,кельвине,кельвинах" + }, + "Time": { + "day.format": "д", + "day.variants": "день,дня,дней,днях,д", + "hour.format": "ч", + "hour.variants": "час,часа,часов,часах", + "minute.format": "мин", + "minute.variants": "мин,мин.,минута,минуты,минут,минутах", + "month.format": "мес.", + "month.variants": "месяц,месяца,месяцев,месяцах,мес,мес.", + "second.format": "с", + "second.symbol": "с", + "second.variants": "сек,секунда,секунды,секунд,секундах,сек.", + "week.format": "нед.", + "week.variants": "неделя,недели,недель,неделях,нед.,нед", + "year.format": "г", + "year.variants": "год,года,лет,годах" + }, + "Volume": { + "cbcm.variants": "см³", + "cbinch.variants": "дюйм³", + "cup.format": "чаш.", + "cup.variants": "чашка,чашек,чашках,чашку,чашкам,чаш.,чашк,чашк.,чаш,чашки", + "gallon.format": "гал.", + "gallon.variants": "галлон,гал.,гал,галлона,галлонов,галлонах,галлоне", + "litre.format": "л", + "litre.symbol": "л", + "litre.variants": "литр,литров,литрах,литра,л", + "pint.format": "пинт.", + "pint.variants": "пинт,пинта,пинтах", + "quart.format": "кварт.", + "quart.variants": "кварта,четверть,кварт.,кварт,квартах,кварте", + "tablespoon.format": "ст. л.", + "tablespoon.variants": "ст. л.,ст. ложка,столовая ложка,столовой ложки,столовой ложке,столовых ложек,столовые ложки,столовыми ложками,столовых ложках,ст.л.", + "teaspoon.format": "ч. л.", + "teaspoon.variants": "ч. л.,ч.л.,чайная ложка,ч. ложка,ч. ложках,ч. ложки,ч. ложками,чайных ложки,чайных ложках,чайной ложке,чайные ложки", + "volume.templates": "cu %@,cubic %@,cb %@,cb%@,%@³,%@ ³,куб. %@,кубический %@" + }, + "Weight": { + "carat.format": "кар", + "carat.variants": "карата,карат,каратах,каратов,кар", + "centner.format": "ц", + "centner.variants": "центнер,центнеров,центнерах,центнера", + "gram.format": "г", + "gram.symbol": "г", + "gram.variants": "грамм,граммов,грамма,граммах", + "ounce.format": "унция", + "ounce.variants": "унция,унциях,унции", + "pound.format": "фунт", + "pound.variants": "фунт,фунтов,фунтах,фунта", + "stone.format": "стоун", + "stone.variants": "стоун,стоунах,стоуна,стоунов", + "tonne.format": "т", + "tonne.symbol": "т", + "tonne.variants": "тонны,тонна,тонн,тоннах" + } +}; diff --git a/src/engine/vocab.ts b/src/engine/vocab.ts new file mode 100644 index 0000000..32604b4 --- /dev/null +++ b/src/engine/vocab.ts @@ -0,0 +1,72 @@ +// Merged phrase lists over the en + ru vocabulary tables. +// Shape: { Category: { "key.variants": "a,b,c", "key.format": "...", ... } } +import { EN, RU } from "./vocab-data"; + +export type VocabCategory = Record; +export type VocabLocale = Record; + +const locales: VocabLocale[] = [EN, RU]; + +/** All values for `category.key` across locales, comma-split, trimmed, deduped. */ +export function variants(category: string, key: string): string[] { + const out: string[] = []; + for (const loc of locales) { + const raw = loc[category]?.[key]; + if (!raw) continue; + for (const v of raw.split(",")) { + const t = v.trim(); + if (t && !out.includes(t)) out.push(t); + } + } + return out; +} + +/** Single value (first locale that has it), e.g. formats. */ +export function entry(category: string, key: string): string | undefined { + for (const loc of locales) { + const raw = loc[category]?.[key]; + if (raw !== undefined) return raw; + } + return undefined; +} + +/** English-only entry (formats should not be localized). */ +export function entryEn(category: string, key: string): string | undefined { + return EN[category]?.[key]; +} + +/** The entry value from every locale that defines it (e.g. unit symbols "m" and "м"). */ +export function entriesAll(category: string, key: string): string[] { + const out: string[] = []; + for (const loc of locales) { + const raw = loc[category]?.[key]; + if (raw && !out.includes(raw)) out.push(raw); + } + return out; +} + +/** All base keys in a category that end with `.variants` (e.g. unit ids). */ +export function idsOf(category: string): string[] { + const ids = new Set(); + for (const loc of locales) { + const cat = loc[category]; + if (!cat) continue; + for (const k of Object.keys(cat)) { + if (k.endsWith(".variants")) ids.add(k.slice(0, -".variants".length)); + } + } + return [...ids]; +} + +/** All keys of a category ending with a suffix, e.g. `.prefix` for SI prefixes. */ +export function idsWithSuffix(category: string, suffix: string): string[] { + const ids = new Set(); + for (const loc of locales) { + const cat = loc[category]; + if (!cat) continue; + for (const k of Object.keys(cat)) { + if (k.endsWith(suffix)) ids.add(k.slice(0, -suffix.length)); + } + } + return [...ids]; +} diff --git a/src/extensions.ts b/src/extensions.ts new file mode 100644 index 0000000..e7d9214 --- /dev/null +++ b/src/extensions.ts @@ -0,0 +1,29 @@ +// JS extensions runtime (Numi-compatible API): numi.setVariable, +// numi.addUnit, numi.addFunction. Scripts share the engine's JS runtime. +import { SumEngine, ExtensionUnitSpec, ExtensionValue } from "./engine"; + +export interface ExtensionApi { + setVariable(name: string, value: number | ExtensionValue): void; + addUnit(spec: ExtensionUnitSpec): void; + addFunction(spec: { id: string; phrases: string }, fn: (values: ExtensionValue[]) => ExtensionValue | number): void; +} + +export function makeApi(engine: SumEngine): ExtensionApi { + return { + setVariable: (name, value) => engine.setVariable(name, value), + addUnit: (spec) => engine.addUnit(spec), + addFunction: (spec, fn) => engine.addFunction(spec, fn), + }; +} + +export function runExtensions(engine: SumEngine, scripts: { name: string; code: string }[]): void { + const numi = makeApi(engine); + for (const s of scripts) { + try { + const fn = new Function("numi", s.code); + fn(numi); + } catch (e) { + console.error(`extension ${s.name} failed:`, e); + } + } +} diff --git a/src/i18n.ts b/src/i18n.ts new file mode 100644 index 0000000..490bc0e --- /dev/null +++ b/src/i18n.ts @@ -0,0 +1,101 @@ +/** UI localization (en/ru). The engine itself understands both languages at once. */ + +const dict: Record> = { + en: { + settings: "Settings", + theme: "Theme", + system: "System", + light: "Light", + dark: "Dark", + precision: "Decimal places", + groupsep: "Thousands separator", + language: "Language", + hotkey: "Global hotkey", + autostart: "Launch at startup", + fontsize: "Font size", + extensions: "Extensions folder", + done: "Done", + newDocument: "New document", + untitled: "Untitled", + copied: "Copied", + ratesUpdated: "Rates updated", + ratesOffline: "Offline rates", + deleteDoc: "Delete document?", + pressKeys: "Press keys…", + total: "Total", + resultswidth: "Results column", + selection: "Selection", + rates: "Rates", + justNow: "just now", + minAgo: "{}m ago", + hourAgo: "{}h ago", + dayAgo: "{}d ago", + refreshRates: "Click to refresh", + bindays: "Bin retention (days)", + datadir: "Data folder", + backups: "Backups", + defaultFolder: "Default", + folderConflict: "The selected folder already contains Summarum sheets. Use them, or replace with the current ones?", + useExisting: "Use existing", + replaceMine: "Replace with mine", + folderChanged: "Data folder changed", + }, + ru: { + settings: "Настройки", + theme: "Тема", + system: "Системная", + light: "Светлая", + dark: "Тёмная", + precision: "Знаков после запятой", + groupsep: "Разделитель тысяч", + language: "Язык", + hotkey: "Глобальный хоткей", + autostart: "Запуск при старте Windows", + fontsize: "Размер шрифта", + extensions: "Папка расширений", + done: "Готово", + newDocument: "Новый документ", + untitled: "Без названия", + copied: "Скопировано", + ratesUpdated: "Курсы обновлены", + ratesOffline: "Курсы офлайн", + deleteDoc: "Удалить документ?", + pressKeys: "Нажмите клавиши…", + total: "Сумма", + resultswidth: "Колонка результатов", + selection: "Выделение", + rates: "Курсы", + justNow: "только что", + minAgo: "{} мин назад", + hourAgo: "{} ч назад", + dayAgo: "{} дн назад", + refreshRates: "Клик — обновить", + bindays: "Корзина (дней)", + datadir: "Папка данных", + backups: "Бэкапы", + defaultFolder: "По умолчанию", + folderConflict: "В выбранной папке уже есть листы Summarum. Использовать их или заменить текущими?", + useExisting: "Использовать их", + replaceMine: "Заменить моими", + folderChanged: "Папка данных изменена", + }, +}; + +let lang = "en"; + +export function setLang(l: string): void { + lang = dict[l] ? l : "en"; + document.documentElement.lang = lang; + for (const el of document.querySelectorAll("[data-i18n]")) { + const key = el.dataset.i18n!; + el.textContent = t(key); + } +} + +export function t(key: string): string { + return dict[lang]?.[key] ?? dict.en[key] ?? key; +} + +export function detectLang(): string { + return navigator.language?.toLowerCase().startsWith("ru") ? "ru" : "en"; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..a017af9 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,576 @@ +/** App bootstrap: engine + editor + documents + settings + Tauri glue. */ +import "./ui/app.css"; +import { SumEngine } from "./engine"; +import { SumEditor } from "./ui/editor"; +import { + AppData, DocMeta, SettingsData, defaultSettingsData, + loadAppData, saveAppData, loadSettings, saveSettings, + fetchRates, loadExtensionScripts, openExtensionsFolder, isTauri, + getLaunchFile, onOpenFile, onFileDrop, + setDataDir, runBackups, backupDeletedSheet, openBackupsFolder, + chooseFolder, dataDirHasDocuments, migrateDataDir, +} from "./storage"; +import type { LineResult } from "./engine"; +import { setLang, detectLang, t } from "./i18n"; +import { runExtensions } from "./extensions"; +import * as vocabApi from "./engine/vocab"; + +const WELCOME = (vocabApi.entry("Samples", "sample.welcome") ?? "# Sample\n8 / (45 - 20%)\n5 inches in cm\n$9 in Euro") + "\n"; + +const $ = (sel: string): T => document.querySelector(sel) as T; + +let engine: SumEngine; +let editor: SumEditor; +let settings: SettingsData; +let data: AppData; +let lastResults: LineResult[] = []; +let selectedRange: [number, number] | null = null; +let ratesFetchedAt = 0; + +function uid(): string { + return Math.random().toString(36).slice(2, 10); +} + +// ---------- documents + +function activeDoc(): DocMeta { + return data.docs.find((d) => d.id === data.activeId) ?? data.docs[0]; +} + +function titleFromContent(text: string): string { + for (const line of text.split("\n")) { + const s = line.trim(); + if (s.length > 0) return s.replace(/^#\s*/, "").slice(0, 30); + } + return t("untitled"); +} + +function syncTitleField(): void { + const field = $("#doc-title"); + if (document.activeElement !== field) field.value = activeDoc()?.title ?? ""; +} + +function renderDocList(): void { + const list = $("#doc-list"); + list.replaceChildren(); + for (const doc of data.docs) { + const el = document.createElement("div"); + el.className = "doc-item" + (doc.id === data.activeId ? " active" : ""); + const name = document.createElement("span"); + name.textContent = doc.title || t("untitled"); + el.appendChild(name); + const del = document.createElement("button"); + del.className = "del"; + del.textContent = "✕"; + // two-step inline confirmation instead of the native dialog + let confirmTimer: ReturnType | null = null; + del.addEventListener("click", (e) => { + e.stopPropagation(); + if (data.docs.length === 1) return; + if (!del.classList.contains("confirm")) { + del.classList.add("confirm"); + del.textContent = "✓"; + confirmTimer = setTimeout(() => { + del.classList.remove("confirm"); + del.textContent = "✕"; + }, 2000); + return; + } + if (confirmTimer) clearTimeout(confirmTimer); + // the sheet goes to backups/deleted/ — restore by dragging it back + void backupDeletedSheet(settings.dataDir, doc.title, data.contents[doc.id] ?? ""); + delete data.contents[doc.id]; + data.docs = data.docs.filter((d) => d.id !== doc.id); + if (data.activeId === doc.id) switchDoc(data.docs[0].id); + else renderDocList(); + saveAppData(data); + }); + el.appendChild(del); + el.addEventListener("click", () => switchDoc(doc.id)); + list.appendChild(el); + } +} + +function switchDoc(id: string): void { + data.activeId = id; + editor.setText(data.contents[id] ?? ""); + syncTitleField(); + renderDocList(); + saveAppData(data); + editor.focus(); +} + +function newDoc(content = ""): void { + const id = uid(); + data.docs.push({ id, title: content ? titleFromContent(content) : t("untitled") }); + data.contents[id] = content; + switchDoc(id); +} + +// ---------- settings + +function applySettings(): void { + const theme = settings.theme === "system" + ? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light") + : settings.theme; + document.documentElement.dataset.theme = theme; + document.documentElement.style.setProperty("--editor-font-size", `${settings.fontSize}px`); + $("#editor-wrap").style.setProperty("--results-width", `${settings.resultsWidth}%`); + setLang(settings.language); + engine.updateSettings({ precision: settings.precision, groupSeparator: settings.groupSeparator }); + editor?.refresh(); + syncTitleField(); + $("#sidebar").classList.toggle("hidden", !settings.sidebarVisible); +} + +function bindSettingsUI(): void { + const themeSel = $("#set-theme"); + const precision = $("#set-precision"); + const groupSep = $("#set-groupsep"); + const langSel = $("#set-lang"); + const hotkey = $("#set-hotkey"); + const autostart = $("#set-autostart"); + const fontSize = $("#set-fontsize"); + const resultsWidth = $("#set-resultswidth"); + + themeSel.value = settings.theme; + precision.value = String(settings.precision); + groupSep.value = settings.groupSeparator; + langSel.value = settings.language; + hotkey.value = settings.hotkey; + autostart.checked = settings.autostart; + fontSize.value = String(settings.fontSize); + resultsWidth.value = String(settings.resultsWidth); + + const save = () => { + settings.theme = themeSel.value as SettingsData["theme"]; + settings.precision = Math.max(0, Math.min(15, Number(precision.value) || 2)); + settings.groupSeparator = groupSep.value; + settings.language = langSel.value; + settings.fontSize = Math.max(10, Math.min(32, Number(fontSize.value) || 15)); + settings.resultsWidth = Math.max(20, Math.min(60, Number(resultsWidth.value) || 42)); + applySettings(); + void saveSettings(settings); + }; + for (const el of [themeSel, precision, groupSep, langSel, fontSize]) { + el.addEventListener("change", save); + } + resultsWidth.addEventListener("input", save); // live while sliding + // hotkey is recorded, not typed + hotkey.readOnly = true; + hotkey.addEventListener("focus", () => { + hotkey.value = ""; + hotkey.placeholder = t("pressKeys"); + }); + hotkey.addEventListener("blur", () => { + hotkey.value = settings.hotkey; + hotkey.placeholder = defaultSettingsData.hotkey; + }); + hotkey.addEventListener("keydown", async (e) => { + e.preventDefault(); + e.stopPropagation(); + if (e.key === "Escape") { + hotkey.blur(); + return; + } + const mods = [ + e.ctrlKey && "Ctrl", + e.altKey && "Alt", + e.shiftKey && "Shift", + e.metaKey && "Super", + ].filter(Boolean) as string[]; + if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) { + hotkey.value = mods.length ? mods.join("+") + "+…" : ""; + return; + } + if (mods.length === 0) return; // a bare key is not a global hotkey + const keyName = normalizeKeyName(e); + if (!keyName) return; + const combo = [...mods, keyName].join("+"); + const old = settings.hotkey; + settings.hotkey = combo; + hotkey.value = combo; + await registerHotkey(old, combo); + void saveSettings(settings); + hotkey.blur(); + }); + autostart.addEventListener("change", async () => { + settings.autostart = autostart.checked; + await applyAutostart(settings.autostart); + void saveSettings(settings); + }); + + const binDays = $("#set-bindays"); + binDays.value = String(settings.deletedRetentionDays); + binDays.addEventListener("change", () => { + settings.deletedRetentionDays = Math.max(1, Math.min(365, Math.round(Number(binDays.value)) || 14)); + binDays.value = String(settings.deletedRetentionDays); + void saveSettings(settings); + void runBackups(settings.dataDir, settings.deletedRetentionDays); // prune right away + }); + + const dataDirBtn = $("#set-datadir"); + const renderDataDir = () => { + dataDirBtn.textContent = settings.dataDir ? settings.dataDir.split(/[\\/]/).pop() ?? settings.dataDir : t("defaultFolder"); + dataDirBtn.title = settings.dataDir || t("defaultFolder"); + }; + renderDataDir(); + dataDirBtn.addEventListener("click", async () => { + const picked = await chooseFolder(); + if (!picked || picked === settings.dataDir) return; + let strategy: "move" | "overwrite" | "use_existing" = "move"; + if (await dataDirHasDocuments(picked)) { + const ans = await askModal(t("folderConflict"), t("useExisting"), t("replaceMine")); + if (ans === null) return; + strategy = ans === "a" ? "use_existing" : "overwrite"; + } + await migrateDataDir(settings.dataDir, picked, strategy); + settings.dataDir = picked; + setDataDir(picked); + await saveSettings(settings); + renderDataDir(); + if (strategy === "use_existing") { + const fresh = await loadAppData(picked); + if (fresh && fresh.docs.length > 0) { + data = fresh; + if (!data.docs.some((d) => d.id === data.activeId)) data.activeId = data.docs[0].id; + switchDoc(data.activeId); + } + } + toast(t("folderChanged")); + }); + + $("#open-settings").addEventListener("click", () => { + $("#settings-panel").classList.toggle("hidden"); + }); + $("#close-settings").addEventListener("click", () => { + $("#settings-panel").classList.add("hidden"); + }); + $("#open-extensions").addEventListener("click", () => void openExtensionsFolder()); + $("#open-backups").addEventListener("click", () => void openBackupsFolder(settings.dataDir)); + + matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { + if (settings.theme === "system") applySettings(); + }); +} + +/** Physical key (e.code) → Tauri accelerator name, layout-independent (works on ru). */ +function normalizeKeyName(e: KeyboardEvent): string | null { + const code = e.code; + if (/^Key[A-Z]$/.test(code)) return code.slice(3); + if (/^Digit\d$/.test(code)) return code.slice(5); + if (/^Numpad\d$/.test(code)) return code; + if (/^F\d{1,2}$/.test(code)) return code; + const map: Record = { + Space: "Space", Enter: "Enter", Backspace: "Backspace", Delete: "Delete", + Tab: "Tab", Home: "Home", End: "End", PageUp: "PageUp", PageDown: "PageDown", + ArrowUp: "Up", ArrowDown: "Down", ArrowLeft: "Left", ArrowRight: "Right", + Minus: "-", Equal: "=", Comma: ",", Period: ".", Slash: "/", Backquote: "`", + BracketLeft: "[", BracketRight: "]", Semicolon: ";", Quote: "'", + }; + return map[code] ?? null; +} + +// ---------- tauri integration + +async function registerHotkey(old: string | null, combo: string): Promise { + if (!isTauri()) return; + try { + const gs = await import("@tauri-apps/plugin-global-shortcut"); + if (old) await gs.unregister(old).catch(() => {}); + await gs.register(combo, async (e) => { + if (e.state !== "Pressed") return; + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + const win = getCurrentWindow(); + if (await win.isVisible() && await win.isFocused()) { + await win.hide(); + } else { + await win.show(); + await win.setFocus(); + } + }); + } catch (e) { + console.warn("hotkey registration failed", e); + } +} + +async function applyAutostart(enabled: boolean): Promise { + if (!isTauri()) return; + try { + const auto = await import("@tauri-apps/plugin-autostart"); + if (enabled) await auto.enable(); + else await auto.disable(); + } catch (e) { + console.warn("autostart failed", e); + } +} + +async function refreshRates(force = false): Promise { + const info = $("#rates-info"); + info.classList.add("spin"); + const payload = await fetchRates(force); + info.classList.remove("spin"); + if (payload) { + engine.setRates(payload.rates); + editor.refresh(); + ratesFetchedAt = payload.fetchedAt; + if (force) toast(t("ratesUpdated")); + } + renderRatesInfo(); +} + +// ---------- status bar + +/** total of the selection (≥2 lines) or of the whole sheet */ +function renderTotal(): void { + const line = $("#total-line"); + let results = lastResults; + let label = t("total"); + if (selectedRange && selectedRange[1] > selectedRange[0]) { + results = lastResults.slice(selectedRange[0], selectedRange[1] + 1); + label = t("selection"); + } + const total = engine.totalOf(results); + if (total) { + line.innerHTML = ""; + const lbl = document.createElement("span"); + lbl.className = "total-label"; + lbl.textContent = label; + line.append(lbl, document.createTextNode(total)); + line.dataset.value = total; + line.style.display = ""; + } else { + line.style.display = "none"; + delete line.dataset.value; + } +} + +function agoText(unixSec: number): string { + const s = Math.max(0, Math.floor(Date.now() / 1000) - unixSec); + if (s < 90) return t("justNow"); + if (s < 3600) return t("minAgo").replace("{}", String(Math.round(s / 60))); + if (s < 86400) return t("hourAgo").replace("{}", String(Math.round(s / 3600))); + return t("dayAgo").replace("{}", String(Math.round(s / 86400))); +} + +function renderRatesInfo(): void { + const el = $("#rates-info"); + if (ratesFetchedAt > 0) { + el.textContent = `${t("rates")}: ${agoText(ratesFetchedAt)}`; + el.title = t("refreshRates"); + } else { + el.textContent = `${t("rates")}: ⚠ offline`; + el.title = t("ratesOffline"); + } +} + +// ---------- modal + +/** two-button modal; resolves "a" | "b" | null (click outside = cancel) */ +function askModal(msg: string, aLabel: string, bLabel: string): Promise<"a" | "b" | null> { + return new Promise((resolve) => { + const modal = $("#modal"); + const a = $("#modal-a"); + const b = $("#modal-b"); + $("#modal-msg").textContent = msg; + a.textContent = aLabel; + b.textContent = bLabel; + a.classList.add("primary"); + modal.classList.remove("hidden"); + const done = (res: "a" | "b" | null) => { + modal.classList.add("hidden"); + a.onclick = b.onclick = modal.onclick = null; + resolve(res); + }; + a.onclick = () => done("a"); + b.onclick = () => done("b"); + modal.onclick = (e) => { + if (e.target === modal) done(null); + }; + }); +} + +// ---------- toast + +let toastTimer: ReturnType | null = null; +function toast(msg: string): void { + const el = $("#toast"); + el.textContent = msg; + el.classList.remove("hidden"); + if (toastTimer) clearTimeout(toastTimer); + toastTimer = setTimeout(() => el.classList.add("hidden"), 1200); +} + +// ---------- boot + +async function boot(): Promise { + settings = await loadSettings(); + if (!localStorage.getItem("numi.langInit") && settings.language === "en") { + settings.language = detectLang(); + localStorage.setItem("numi.langInit", "1"); + } + + engine = new SumEngine({ precision: settings.precision, groupSeparator: settings.groupSeparator }); + + // snapshots run before the app writes anything + setDataDir(settings.dataDir); + await runBackups(settings.dataDir, settings.deletedRetentionDays); + + const stored = await loadAppData(settings.dataDir); + if (stored && stored.docs.length > 0) { + data = stored; + } else { + const id = uid(); + data = { docs: [{ id, title: "Sample" }], activeId: id, contents: { [id]: WELCOME } }; + } + if (!data.docs.some((d) => d.id === data.activeId)) data.activeId = data.docs[0].id; + + const scripts = await loadExtensionScripts(); + runExtensions(engine, scripts); + + editor = new SumEditor( + $("#editor"), + $("#results"), + engine, + { + onChange(text) { + const doc = activeDoc(); + data.contents[doc.id] = text; + if (!doc.customTitle) { + doc.title = titleFromContent(text); + syncTitleField(); + } + renderDocList(); + saveAppData(data); + }, + onCopy(text) { + void navigator.clipboard.writeText(text); + toast(t("copied")); + }, + onResults(results) { + lastResults = results; + renderTotal(); + }, + onSelection(range) { + selectedRange = range; + renderTotal(); + }, + }, + data.contents[data.activeId] ?? "", + ); + + applySettings(); // sets the language before bindSettingsUI renders dynamic labels + bindSettingsUI(); + renderDocList(); + syncTitleField(); + + const titleField = $("#doc-title"); + titleField.addEventListener("change", () => { + const doc = activeDoc(); + const v = titleField.value.trim(); + if (v) { + doc.title = v.slice(0, 60); + doc.customTitle = true; + } else { + doc.customTitle = false; + doc.title = titleFromContent(data.contents[doc.id] ?? ""); + } + syncTitleField(); + renderDocList(); + saveAppData(data); + }); + titleField.addEventListener("keydown", (e) => { + if (e.key === "Enter" || e.key === "Escape") titleField.blur(); + }); + + $("#total-line").addEventListener("click", () => { + const v = $("#total-line").dataset.value; + if (v) { + void navigator.clipboard.writeText(v); + toast(t("copied")); + } + }); + + if (isTauri()) { + const { getCurrentWindow } = await import("@tauri-apps/api/window"); + const win = getCurrentWindow(); + $("#win-min").addEventListener("click", () => void win.minimize()); + $("#win-close").addEventListener("click", () => void win.hide()); + } else { + $("#win-controls").style.display = "none"; + } + + $("#toggle-sidebar").addEventListener("click", () => { + settings.sidebarVisible = !settings.sidebarVisible; + $("#sidebar").classList.toggle("hidden", !settings.sidebarVisible); + void saveSettings(settings); + }); + $("#new-doc").addEventListener("click", () => newDoc()); + + document.addEventListener("mousedown", (e) => { + if (!settings.sidebarVisible) return; + const target = e.target as HTMLElement; + if (target.closest("#sidebar") || target.closest("#toggle-sidebar")) return; + settings.sidebarVisible = false; + $("#sidebar").classList.add("hidden"); + void saveSettings(settings); + }); + + const divider = $("#col-divider"); + const wrap = $("#editor-wrap"); + divider.addEventListener("mousedown", (e) => { + e.preventDefault(); + divider.classList.add("dragging"); + document.body.style.cursor = "col-resize"; + const onMove = (ev: MouseEvent) => { + const rect = wrap.getBoundingClientRect(); + const pct = ((rect.right - ev.clientX) / rect.width) * 100; + settings.resultsWidth = Math.round(Math.max(20, Math.min(60, pct))); + wrap.style.setProperty("--results-width", `${settings.resultsWidth}%`); + }; + const onUp = () => { + window.removeEventListener("mousemove", onMove); + window.removeEventListener("mouseup", onUp); + divider.classList.remove("dragging"); + document.body.style.cursor = ""; + $("#set-resultswidth").value = String(settings.resultsWidth); + void saveSettings(settings); + }; + window.addEventListener("mousemove", onMove); + window.addEventListener("mouseup", onUp); + }); + + document.addEventListener("keydown", (e) => { + if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "n") { + e.preventDefault(); + newDoc(); + } + if (e.ctrlKey && !e.shiftKey && e.key.toLowerCase() === "b") { + e.preventDefault(); + $("#toggle-sidebar").click(); + } + if (e.ctrlKey && e.shiftKey && e.code === "KeyC") { + e.preventDefault(); + void navigator.clipboard.writeText(editor.getSheetWithResults()); + toast(t("copied")); + } + }); + + $("#rates-info").addEventListener("click", () => void refreshRates(true)); + setInterval(renderRatesInfo, 60_000); + + void onFileDrop((content) => newDoc(content)); + + await registerHotkey(null, settings.hotkey); + if (settings.autostart) await applyAutostart(true); + void refreshRates(); + setInterval(() => void refreshRates(), 60 * 60 * 1000); + + const launched = await getLaunchFile(); + if (launched) newDoc(launched); + void onOpenFile((content) => newDoc(content)); + + editor.focus(); +} + +void boot(); diff --git a/src/storage.ts b/src/storage.ts new file mode 100644 index 0000000..50ab269 --- /dev/null +++ b/src/storage.ts @@ -0,0 +1,241 @@ +// Storage: Tauri commands in the app, localStorage fallback for `vite dev`. + +export interface DocMeta { + id: string; + title: string; + /** user renamed the document — stop deriving the title from the first line */ + customTitle?: boolean; +} + +export interface AppData { + docs: DocMeta[]; + activeId: string; + contents: Record; +} + +export interface SettingsData { + theme: "system" | "light" | "dark"; + precision: number; + groupSeparator: string; + language: string; + hotkey: string; + autostart: boolean; + fontSize: number; + sidebarVisible: boolean; + /** results column width, % of the editor area */ + resultsWidth: number; + /** custom folder for documents.json + backups; "" = default app data dir */ + dataDir: string; + /** how long deleted sheets stay in backups/deleted */ + deletedRetentionDays: number; +} + +export const defaultSettingsData: SettingsData = { + theme: "system", + precision: 2, + groupSeparator: ",", + language: "en", + hotkey: "Ctrl+Alt+N", + autostart: false, + fontSize: 15, + sidebarVisible: false, + resultsWidth: 42, + dataDir: "", + deletedRetentionDays: 14, +}; + +/** "" -> undefined for the Rust side (default app data dir) */ +function dirArg(dataDir: string): string | null { + return dataDir.trim() ? dataDir : null; +} + +export function isTauri(): boolean { + return "__TAURI_INTERNALS__" in window; +} + +async function invoke(cmd: string, args?: Record): Promise { + const { invoke } = await import("@tauri-apps/api/core"); + return invoke(cmd, args); +} + +// ---------- settings + +export async function loadSettings(): Promise { + try { + const raw = isTauri() + ? await invoke("load_file", { name: "settings.json" }) + : localStorage.getItem("numi.settings"); + if (raw) return { ...defaultSettingsData, ...JSON.parse(raw) }; + } catch (e) { + console.warn("loadSettings failed", e); + } + return { ...defaultSettingsData }; +} + +export async function saveSettings(s: SettingsData): Promise { + const raw = JSON.stringify(s, null, 2); + if (isTauri()) await invoke("save_file", { name: "settings.json", contents: raw }); + else localStorage.setItem("numi.settings", raw); +} + +// ---------- documents + +export async function loadAppData(dataDir: string): Promise { + try { + const raw = isTauri() + ? await invoke("load_documents", { dir: dirArg(dataDir) }) + : localStorage.getItem("numi.documents"); + if (raw) return JSON.parse(raw); + } catch (e) { + console.warn("loadAppData failed", e); + } + return null; +} + +let saveTimer: ReturnType | null = null; +let currentDataDir = ""; + +export function setDataDir(dataDir: string): void { + currentDataDir = dataDir; +} + +export function saveAppData(data: AppData): void { + // debounce: autosave on every keystroke without disk churn + if (saveTimer) clearTimeout(saveTimer); + saveTimer = setTimeout(async () => { + const raw = JSON.stringify(data); + try { + if (isTauri()) await invoke("save_documents", { dir: dirArg(currentDataDir), contents: raw }); + else localStorage.setItem("numi.documents", raw); + } catch (e) { + console.warn("saveAppData failed", e); + } + }, 400); +} + +// ---------- backups & data folder + +export async function runBackups(dataDir: string, retentionDays: number): Promise { + if (!isTauri()) return; + try { + await invoke("run_backups", { dir: dirArg(dataDir), retentionDays }); + } catch (e) { + console.warn("runBackups failed", e); + } +} + +export async function backupDeletedSheet(dataDir: string, title: string, contents: string): Promise { + if (!isTauri() || !contents.trim()) return; + try { + await invoke("backup_deleted_sheet", { dir: dirArg(dataDir), title, contents }); + } catch (e) { + console.warn("backupDeletedSheet failed", e); + } +} + +export async function openBackupsFolder(dataDir: string): Promise { + if (isTauri()) await invoke("open_backups_folder", { dir: dirArg(dataDir) }); +} + +export async function chooseFolder(): Promise { + if (!isTauri()) return null; + const { open } = await import("@tauri-apps/plugin-dialog"); + const picked = await open({ directory: true, multiple: false }); + return typeof picked === "string" ? picked : null; +} + +export async function dataDirHasDocuments(dir: string): Promise { + if (!isTauri()) return false; + return invoke("data_dir_has_documents", { dir }); +} + +export type MigrateStrategy = "move" | "overwrite" | "use_existing"; + +export async function migrateDataDir(oldDir: string, newDir: string, strategy: MigrateStrategy): Promise { + if (!isTauri()) return; + await invoke("migrate_data_dir", { oldDir: dirArg(oldDir), newDir, strategy }); +} + +// ---------- rates + +export interface RatesPayload { + date: string; + rates: Record; + /** unix seconds when the rates were actually fetched */ + fetchedAt: number; +} + +export async function fetchRates(force = false): Promise { + try { + if (isTauri()) { + return await invoke("fetch_rates", { force }); + } + const res = await fetch("https://open.er-api.com/v6/latest/USD"); + const json = await res.json(); + if (json?.result === "success") { + return { date: json.time_last_update_utc, rates: json.rates, fetchedAt: Math.floor(Date.now() / 1000) }; + } + } catch (e) { + console.warn("fetchRates failed", e); + } + return null; +} + +// ---------- extensions + +export async function loadExtensionScripts(): Promise<{ name: string; code: string }[]> { + if (!isTauri()) return []; + try { + return await invoke<{ name: string; code: string }[]>("load_extensions"); + } catch (e) { + console.warn("loadExtensions failed", e); + return []; + } +} + +export async function openExtensionsFolder(): Promise { + if (isTauri()) await invoke("open_extensions_folder"); +} + +// ---------- .numi file association + +export async function getLaunchFile(): Promise { + if (!isTauri()) return null; + try { + return await invoke("get_launch_file"); + } catch { + return null; + } +} + +export async function onOpenFile(cb: (content: string) => void): Promise { + if (!isTauri()) return; + const { listen } = await import("@tauri-apps/api/event"); + await listen("open-file", (e) => cb(e.payload)); +} + +/** Drag & drop of .numi/.txt/.md files: Tauri native events or HTML5 fallback. */ +export async function onFileDrop(cb: (content: string) => void): Promise { + if (isTauri()) { + const { getCurrentWebview } = await import("@tauri-apps/api/webview"); + await getCurrentWebview().onDragDropEvent(async (event) => { + if (event.payload.type !== "drop") return; + for (const path of event.payload.paths) { + try { + const content = await invoke("read_text_file", { path }); + cb(content); + } catch (e) { + console.warn("file drop rejected", path, e); + } + } + }); + return; + } + window.addEventListener("dragover", (e) => e.preventDefault()); + window.addEventListener("drop", async (e) => { + e.preventDefault(); + for (const f of e.dataTransfer?.files ?? []) { + if (/\.(numi|txt|md)$/i.test(f.name)) cb(await f.text()); + } + }); +} diff --git a/src/ui/app.css b/src/ui/app.css new file mode 100644 index 0000000..9ef8d95 --- /dev/null +++ b/src/ui/app.css @@ -0,0 +1,399 @@ +/* Summarum UI styles. */ +:root { + --font: "Segoe UI", system-ui, sans-serif; + --mono: "Cascadia Code", "Consolas", monospace; + --editor-font-size: 15px; +} + +[data-theme="light"] { + color-scheme: light; + --bg: #ffffff; + --bg-soft: #f5f5f4; + --fg: #3d3d3d; + --fg-muted: #9b9b97; + --accent: #d57d2c; + --result: #d57d2c; + --number: #2a72b5; + --unit: #3f9b56; + --operator: #777770; + --comment: #b0b0aa; + --header-bg: #f0efec; + --border: #e4e4e0; + --sel: #cfe3f7; + --caret: #d57d2c; + --toolbar-bg: #fafaf8; + --hover: #ececea; +} + +[data-theme="dark"] { + color-scheme: dark; + --bg: #1e1f22; + --bg-soft: #26272b; + --fg: #d6d6d1; + --fg-muted: #6f7076; + --accent: #e8923c; + --result: #e8923c; + --number: #6cb2f0; + --unit: #6fc784; + --operator: #9a9aa0; + --comment: #5c5d63; + --header-bg: #2b2c31; + --border: #333438; + --sel: #2d4a68; + --caret: #e8923c; + --toolbar-bg: #232428; + --hover: #2f3035; +} + +* { box-sizing: border-box; } +html, body { margin: 0; height: 100%; } +body { + font-family: var(--font); + background: var(--bg); + color: var(--fg); + overflow: hidden; + user-select: none; +} + +#app { display: flex; height: 100vh; } + +/* ---------- sidebar */ +#sidebar { + width: 180px; + min-width: 180px; + background: var(--bg-soft); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + padding: 6px; +} +#sidebar.hidden { display: none; } +#doc-list { flex: 1; overflow-y: auto; } +.doc-item { + padding: 7px 10px; + border-radius: 6px; + cursor: pointer; + font-size: 13px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + gap: 6px; +} +.doc-item:hover { background: var(--hover); } +.doc-item.active { background: var(--sel); } +.doc-item .del { + margin-left: auto; + opacity: 0; + color: var(--fg-muted); + border: none; + background: none; + cursor: pointer; + font-size: 12px; + border-radius: 4px; + padding: 2px 6px; +} +.doc-item:hover .del { opacity: 1; } +.doc-item .del.confirm { + opacity: 1; + background: #d63a3a; + color: #fff; +} +#new-doc { + border: 1px dashed var(--border); + background: none; + color: var(--fg-muted); + border-radius: 6px; + padding: 6px; + cursor: pointer; + font-size: 15px; +} +#new-doc:hover { background: var(--hover); } + +/* ---------- main */ +#content { flex: 1; display: flex; flex-direction: column; min-width: 0; } + +#titlebar { + display: flex; + align-items: center; + gap: 4px; + height: 36px; + padding: 0 0 0 6px; + background: var(--toolbar-bg); + border-bottom: 1px solid var(--border); + flex-shrink: 0; +} +#titlebar button { + border: none; + background: none; + color: var(--fg-muted); + font-size: 14px; + cursor: pointer; + padding: 4px 9px; + border-radius: 5px; +} +#titlebar button:hover { background: var(--hover); color: var(--fg); } +#doc-title { + border: 1px solid transparent; + background: transparent; + color: var(--fg); + font-family: var(--font); + font-size: 13px; + padding: 4px 8px; + border-radius: 5px; + width: 170px; + outline: none; +} +#doc-title:hover { background: var(--hover); } +#doc-title:focus { background: var(--bg); border-color: var(--border); } +.spacer { flex: 1; height: 100%; } +#rates-status { font-size: 11px; color: var(--fg-muted); } + +#win-controls { display: flex; height: 100%; margin-left: 2px; } +#win-controls button { + height: 100%; + width: 42px; + border-radius: 0; + font-size: 12px; + padding: 0; +} +#win-close:hover { background: #d63a3a !important; color: #fff !important; } + +/* ---------- status bar (total of all results on the sheet) */ +#statusbar { + display: flex; + align-items: center; + height: 28px; + padding: 0 14px; + background: var(--toolbar-bg); + border-top: 1px solid var(--border); + flex-shrink: 0; + gap: 8px; +} +#rates-info { + font-size: 11.5px; + color: var(--fg-muted); + cursor: pointer; + white-space: nowrap; +} +#rates-info:hover { color: var(--fg); } +#rates-info.spin { opacity: 0.5; pointer-events: none; } +#total-line { + font-family: var(--mono); + font-size: 13px; + color: var(--result); + cursor: pointer; +} +#total-line:hover { text-decoration: underline; } +#total-line .total-label { color: var(--fg-muted); font-family: var(--font); margin-right: 8px; } + +/* ---------- editor + results */ +#editor-wrap { position: relative; flex: 1; overflow: hidden; --results-width: 42%; } +#editor { height: 100%; } + +/* draggable boundary of the results column */ +#col-divider { + position: absolute; + top: 0; + bottom: 0; + right: var(--results-width); + width: 7px; + margin-right: -3px; + cursor: col-resize; + z-index: 10; +} +#col-divider::after { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 3px; + width: 1px; + background: var(--border); +} +#col-divider:hover::after, #col-divider.dragging::after { + width: 2px; + background: var(--accent); +} + +.cm-editor { height: 100%; background: var(--bg); } +.cm-editor.cm-focused { outline: none; } +.cm-scroller { + font-family: var(--mono); + font-size: var(--editor-font-size); + line-height: 1.7; + padding: 12px 0; +} +/* layout paddings for .cm-content live in the CM theme (editor.ts) — + runtime-injected CM base styles would override them here */ +.cm-content { + caret-color: var(--caret); + color: var(--fg); +} +.cm-line { padding: 0; } +.cm-cursor { border-left-color: var(--caret); } +.cm-selectionBackground, .cm-content ::selection { background: var(--sel) !important; } +.cm-activeLine { background: transparent; } + +/* token colors */ +.tok-number { color: var(--number); } +.tok-unit { color: var(--unit); } +.tok-op { color: var(--operator); } +.tok-comment { color: var(--comment); font-style: italic; } +.tok-header { color: var(--fg); font-weight: 600; } +.tok-var { color: var(--accent); } +.tok-var-active { + background: color-mix(in srgb, var(--accent) 22%, transparent); + border-radius: 3px; +} + +/* results overlay */ +#results { + position: absolute; + top: 0; + right: 0; + width: var(--results-width); + height: 100%; + pointer-events: none; + overflow: hidden; +} +.result-line { + position: absolute; + right: 14px; + max-width: calc(100% - 14px); + font-family: var(--mono); + font-size: var(--editor-font-size); + line-height: 1.7; + color: var(--result); + background: var(--bg); + padding: 0 6px; + border-radius: 4px; + cursor: pointer; + pointer-events: auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.result-line:hover { text-decoration: underline; } + +/* ---------- settings */ +#settings-panel { + position: absolute; + top: 36px; + right: 8px; + width: 280px; + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 10px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.25); + padding: 14px; + z-index: 50; +} +#settings-panel.hidden { display: none; } +#settings-panel h2 { margin: 0 0 10px; font-size: 14px; } +#settings-panel label { + display: flex; + justify-content: space-between; + align-items: center; + font-size: 12.5px; + margin: 8px 0; + gap: 8px; +} +#settings-panel label.row { justify-content: flex-start; } +#settings-panel input[type="number"], #settings-panel input[type="text"], #settings-panel select { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 5px; + padding: 4px 6px; + font-size: 12.5px; + width: 110px; +} +.path-btn { + background: var(--bg); + color: var(--fg); + border: 1px solid var(--border); + border-radius: 5px; + padding: 4px 8px; + font-size: 11.5px; + width: 130px; + cursor: pointer; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + text-align: left; +} +.path-btn:hover { background: var(--hover); } + +.settings-footer { display: flex; justify-content: space-between; gap: 6px; margin-top: 12px; } +.settings-footer button { + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + border-radius: 6px; + padding: 5px 12px; + cursor: pointer; + font-size: 12.5px; +} +.settings-footer button:hover { background: var(--hover); } + +/* ---------- toast */ +#toast { + position: fixed; + bottom: 18px; + left: 50%; + transform: translateX(-50%); + background: var(--fg); + color: var(--bg); + font-size: 12.5px; + padding: 6px 14px; + border-radius: 14px; + opacity: 0.92; + z-index: 100; + transition: opacity 0.2s; +} +#toast.hidden { opacity: 0; pointer-events: none; } + +/* ---------- modal (data folder conflict etc.) */ +#modal { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.45); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; +} +#modal.hidden { display: none; } +.modal-box { + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 10px; + padding: 16px; + max-width: 320px; + box-shadow: 0 8px 30px rgba(0, 0, 0, 0.35); +} +.modal-box p { margin: 0 0 14px; font-size: 13px; line-height: 1.5; } +.modal-btns { display: flex; gap: 8px; justify-content: flex-end; } +.modal-btns button { + border: 1px solid var(--border); + background: var(--bg); + color: var(--fg); + border-radius: 6px; + padding: 5px 12px; + cursor: pointer; + font-size: 12.5px; +} +.modal-btns button:hover { background: var(--hover); } +.modal-btns button.primary { background: var(--accent); color: #fff; border-color: var(--accent); } + +/* autocomplete */ +.cm-tooltip.cm-tooltip-autocomplete { + background: var(--bg-soft); + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; +} +.cm-tooltip-autocomplete ul li { font-family: var(--mono); font-size: 13px; color: var(--fg); } +.cm-tooltip-autocomplete ul li[aria-selected] { background: var(--sel); color: var(--fg); } diff --git a/src/ui/editor.ts b/src/ui/editor.ts new file mode 100644 index 0000000..035563a --- /dev/null +++ b/src/ui/editor.ts @@ -0,0 +1,279 @@ +// CodeMirror editor wired to the engine: evaluates on change, highlights +// engine tokens, renders the results overlay, autocompletes phrases. +import { EditorView, ViewUpdate, Decoration, DecorationSet, keymap, drawSelection } from "@codemirror/view"; +import { EditorState, StateEffect, StateField, RangeSetBuilder } from "@codemirror/state"; +import { defaultKeymap, history, historyKeymap } from "@codemirror/commands"; +import { autocompletion, CompletionContext, CompletionResult, completionKeymap } from "@codemirror/autocomplete"; +import { SumEngine, LineResult } from "../engine"; + +export interface EditorCallbacks { + onChange(text: string): void; + onCopy(text: string): void; + onResults(results: LineResult[]): void; + /** 0-based inclusive line range of the selection, null when collapsed */ + onSelection(range: [number, number] | null): void; +} + +const setResults = StateEffect.define(); + +const resultsField = StateField.define({ + create: () => [], + update(value, tr) { + for (const e of tr.effects) if (e.is(setResults)) value = e.value; + return value; + }, +}); + +function buildDecorations(state: EditorState): DecorationSet { + const results = state.field(resultsField); + const builder = new RangeSetBuilder(); + const doc = state.doc; + for (let i = 0; i < Math.min(results.length, doc.lines); i++) { + const line = doc.line(i + 1); + const r = results[i]; + if (r.kind === "header") { + builder.add(line.from, line.from, Decoration.line({ class: "tok-header" })); + continue; + } + const marks: { from: number; to: number; cls: string }[] = []; + for (const tk of r.tokens) { + let cls: string | null = null; + switch (tk.t) { + case "num": case "const": case "scale": cls = "tok-number"; break; + case "unit": case "currency": case "repr": case "date": cls = "tok-unit"; break; + case "op": case "conv": case "assign": case "pctop": case "percent": case "bang": cls = "tok-op"; break; + case "func": case "agg": cls = "tok-var"; break; + default: cls = null; + } + if (cls && tk.end > tk.start && line.from + tk.end <= line.to) { + marks.push({ from: line.from + tk.start, to: line.from + tk.end, cls }); + } + } + if (r.commentStart !== null) { + marks.push({ from: line.from + r.commentStart, to: line.to, cls: "tok-comment" }); + } + marks.sort((a, b) => a.from - b.from); + let last = line.from; + for (const m of marks) { + if (m.from < last) continue; // overlapping (comment vs tokens) — first wins + builder.add(m.from, m.to, Decoration.mark({ class: m.cls })); + last = m.to; + } + } + return builder.finish(); +} + +const highlightField = StateField.define({ + create: buildDecorations, + update(_value, tr) { + return buildDecorations(tr.state); + }, + provide: (f) => EditorView.decorations.from(f), +}); + +/** Highlights every occurrence of the variable under the cursor. */ +function buildVarHighlight(state: EditorState): DecorationSet { + const builder = new RangeSetBuilder(); + const sel = state.selection.main; + if (!sel.empty) return builder.finish(); + const text = state.doc.toString(); + // expand a word around the cursor + const isWordCh = (ch: string) => /[\p{L}\d_]/u.test(ch); + let a = sel.head; + let b = sel.head; + while (a > 0 && isWordCh(text[a - 1])) a--; + while (b < text.length && isWordCh(text[b])) b++; + if (a === b) return builder.finish(); + const word = text.slice(a, b); + if (!/^[\p{L}_][\p{L}\d_]*$/u.test(word)) return builder.finish(); + // only highlight if the word is actually assigned somewhere in the document + const esc = word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + if (!new RegExp(`^\\s*${esc}\\s*=`, "mu").test(text)) return builder.finish(); + const re = new RegExp(`(?({ + create: buildVarHighlight, + update(_value, tr) { + return buildVarHighlight(tr.state); + }, + provide: (f) => EditorView.decorations.from(f), +}); + +export class SumEditor { + view: EditorView; + private resultsEl: HTMLElement; + private results: LineResult[] = []; + + constructor( + parent: HTMLElement, + resultsEl: HTMLElement, + private engine: SumEngine, + private cb: EditorCallbacks, + initialText: string, + ) { + this.resultsEl = resultsEl; + + const completionSource = (ctx: CompletionContext): CompletionResult | null => { + const word = ctx.matchBefore(/[\p{L}_]+$/u); + if (!word || (word.from === word.to && !ctx.explicit)) return null; + if (word.to - word.from < 2 && !ctx.explicit) return null; + const options = this.engine.completions().map((c) => ({ + label: c.label, + type: c.type === "function" ? "function" : c.type === "keyword" ? "keyword" : "constant", + detail: c.detail, + })); + // document variables + const text = ctx.state.doc.toString(); + for (const m of text.matchAll(/^\s*([\p{L}_][\p{L}\d_]*)\s*=/gmu)) { + options.push({ label: m[1], type: "variable", detail: undefined as unknown as string }); + } + return { from: word.from, options, validFor: /^[\p{L}_]*$/u }; + }; + + this.view = new EditorView({ + parent, + state: EditorState.create({ + doc: initialText, + extensions: [ + history(), + keymap.of([...defaultKeymap, ...historyKeymap, ...completionKeymap]), + autocompletion({ override: [completionSource], activateOnTyping: true }), + resultsField, + highlightField, + varHighlightField, + EditorView.lineWrapping, + drawSelection({ cursorBlinkRate: 1000 }), + // CM injects its base styles at runtime after app.css, so layout + // and cursor styling must live in a theme to take precedence + EditorView.theme({ + ".cm-content": { + // --results-width lives on #editor-wrap; the divider drags it + paddingRight: "var(--results-width, 42%)", + paddingLeft: "16px", + overflowWrap: "anywhere", + }, + ".cm-cursor, .cm-dropCursor": { + borderLeft: "2px solid var(--caret)", + }, + }), + EditorView.updateListener.of((u: ViewUpdate) => { + if (u.docChanged) { + const text = u.state.doc.toString(); + this.evaluate(text); + this.cb.onChange(text); + } + if (u.geometryChanged || u.viewportChanged || u.docChanged) { + this.renderResults(); + } + if (u.selectionSet || u.docChanged) { + const sel = u.state.selection.main; + if (sel.empty) { + this.cb.onSelection(null); + } else { + const from = u.state.doc.lineAt(sel.from).number - 1; + const to = u.state.doc.lineAt(sel.to).number - 1; + this.cb.onSelection([from, to]); + } + } + }), + ], + }), + }); + + this.evaluate(initialText); + requestAnimationFrame(() => this.renderResults()); + this.view.scrollDOM.addEventListener("scroll", () => this.renderResults()); + + // copying whole lines also copies their results: "5+5 = 10"; + // a partial single-line selection copies as usual + this.view.dom.addEventListener("copy", (e) => { + const state = this.view.state; + const sel = state.selection.main; + const doc = state.doc; + const lineFrom = doc.lineAt(sel.from); + const lineTo = doc.lineAt(sel.to); + const multiLine = lineFrom.number !== lineTo.number; + const coversLine = sel.from <= lineFrom.from && sel.to >= lineTo.to && !sel.empty; + if (!sel.empty && !multiLine && !coversLine) return; + const lines: string[] = []; + for (let n = lineFrom.number; n <= lineTo.number; n++) { + const line = doc.line(n); + const res = this.results[n - 1]?.text; + lines.push(res ? `${line.text} = ${res}` : line.text); + } + e.clipboardData?.setData("text/plain", lines.join("\n")); + e.preventDefault(); + }); + + this.resultsEl.addEventListener("click", (e) => { + const el = (e.target as HTMLElement).closest(".result-line") as HTMLElement | null; + if (el?.dataset.value) this.cb.onCopy(el.dataset.value); + }); + } + + setText(text: string): void { + this.view.dispatch({ + changes: { from: 0, to: this.view.state.doc.length, insert: text }, + }); + } + + getText(): string { + return this.view.state.doc.toString(); + } + + /** whole sheet with results appended: for Ctrl+Shift+C export */ + getSheetWithResults(): string { + const doc = this.view.state.doc; + const lines: string[] = []; + for (let n = 1; n <= doc.lines; n++) { + const res = this.results[n - 1]?.text; + const text = doc.line(n).text; + lines.push(res ? `${text} = ${res}` : text); + } + return lines.join("\n"); + } + + /** re-evaluate with current engine state (settings/rates changed) */ + refresh(): void { + this.evaluate(this.getText()); + this.renderResults(); + } + + focus(): void { + this.view.focus(); + } + + private evaluate(text: string): void { + this.results = this.engine.evaluateDocument(text); + this.view.dispatch({ effects: setResults.of(this.results) }); + this.cb.onResults(this.results); + } + + private renderResults(): void { + const frag = document.createDocumentFragment(); + const doc = this.view.state.doc; + // documentTop is in screen coordinates; place results relative to the overlay + const overlayTop = this.resultsEl.getBoundingClientRect().top; + const docTop = this.view.documentTop; + for (let i = 0; i < Math.min(this.results.length, doc.lines); i++) { + const r = this.results[i]; + if (!r.text) continue; + const line = doc.line(i + 1); + const block = this.view.lineBlockAt(line.from); + const top = docTop + block.top - overlayTop; + if (top < -40 || top > this.resultsEl.clientHeight + 40) continue; + const el = document.createElement("div"); + el.className = "result-line"; + el.textContent = r.text; + el.dataset.value = r.text; + el.style.top = `${top}px`; + frag.appendChild(el); + } + this.resultsEl.replaceChildren(frag); + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..10a4dc0 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "skipLibCheck": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "useDefineForClassFields": true, + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": ["src", "scripts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..9aa0e07 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + clearScreen: false, + server: { + port: 1420, + strictPort: true, + }, + build: { + target: "es2022", + outDir: "dist", + }, +});