From 268fd49741dd9f17bbeb34a485403a3d3b3ffe11 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Thu, 26 Mar 2026 15:07:04 +0100 Subject: [PATCH] docs: add converter plugin design spec --- .../2026-03-26-converter-plugin-design.md | 419 ++++++++++++++++++ 1 file changed, 419 insertions(+) create mode 100644 docs/superpowers/specs/2026-03-26-converter-plugin-design.md diff --git a/docs/superpowers/specs/2026-03-26-converter-plugin-design.md b/docs/superpowers/specs/2026-03-26-converter-plugin-design.md new file mode 100644 index 0000000..79b7a28 --- /dev/null +++ b/docs/superpowers/specs/2026-03-26-converter-plugin-design.md @@ -0,0 +1,419 @@ +# Converter Plugin — Design Spec + +**Date:** 2026-03-26 +**Status:** Draft +**Goal:** Create an owlry plugin for unit and currency conversion with natural speech input. + +--- + +## 1. Overview + +A dynamic plugin that converts between units of measurement and currencies. Users type natural expressions like `102F to C` or `> 50 kg in lb` and see conversion results inline. + +**Key UX decisions:** +- Trigger prefix `>` for explicit conversion, plus auto-detection +- Accepts `to` and `in` as connector words +- No space required between number and unit (`102F` works) +- When no target unit specified, shows the most common conversions for that category +- Currency rates from ECB (free, no API key, ~30 fiat currencies, daily) +- Case-insensitive matching with generous aliases + +--- + +## 2. Plugin Identity + +| Field | Value | +|-------|-------| +| Plugin ID | `converter` | +| Provider ID | `converter` | +| Type ID | `conv` | +| Provider Kind | Dynamic | +| Trigger prefix | `>` | +| Search prefix | `:conv` | +| Position | Normal | +| Priority | 9000 | +| Icon | `accessories-text-editor` (or `edit-find-replace`) | +| Crate | `owlry-plugin-converter` | +| Repo | `owlry-plugins` | + +--- + +## 3. Query Parsing + +### Input patterns + +All of these are valid and must parse correctly: + +``` +102F to C → 102 Fahrenheit → Celsius +102 F to C → same +102 fahrenheit in celsius → same (full names) +102°F to °C → same (symbols) +> 102 F → prefix, no target → show all temperature conversions +102 F → auto-detect, no target → show all +102F → auto-detect, no space, no target → show all +> 50 kg in lb → prefix, specific target +50 kg → auto-detect, show common weight conversions +100 eur to usd → currency +100€ to $ → currency with symbols +``` + +### Parser rules + +1. Strip prefix `>` if present (and trim whitespace) +2. Extract number: integer or decimal at start of input (`102`, `3.5`, `.5`, `0.25`) +3. Extract source unit: immediately adjacent to number OR space-separated. Match against known unit aliases (case-insensitive, longest match first) +4. If remaining input contains `to` or `in` (case-insensitive, word boundary), extract target unit from what follows +5. If no connector word and remaining text after source unit is non-empty, try to parse it as a target unit anyway (handles `100 km miles`) + +### Auto-detection + +The plugin's `query()` is called for every keystroke. To avoid false positives: +- Only match if the input starts with a number +- Only match if the text after the number resolves to a known unit alias +- Minimum input length: 2 characters (number + unit) +- Don't match if the text after the number is not a unit alias (e.g., `100x`, `102nd`) + +### Ambiguity resolution + +| Input | Resolves to | Why | +|-------|-------------|-----| +| `oz` | Ounce (weight) | More common than fluid ounce | +| `fl oz`, `floz` | Fluid ounce | Explicit | +| `ton` | Metric ton (1000 kg) | International default | +| `short_ton`, `ton_us` | US short ton (907 kg) | Explicit | +| `gal` | US gallon | More common globally in software | +| `gal_uk`, `imp_gal` | Imperial gallon | Explicit | + +--- + +## 4. Unit System Architecture + +### Conversion model + +Each category has a **base unit**. Every unit defines a conversion factor relative to the base: + +``` +value_in_base = input_value * from_unit.to_base(input_value) +result = target_unit.from_base(value_in_base) +``` + +For most units this is a simple multiply/divide by factor. Temperature uses offset formulas. + +### Unit definition + +```rust +struct UnitDef { + id: &'static str, // canonical name: "kilometer" + symbol: &'static str, // display symbol: "km" + aliases: &'static [&'static str], // match aliases: ["km", "kms", "kilometers", "kilometre"] + category: Category, + conversion: Conversion, +} + +enum Conversion { + Factor(f64), // multiply by factor to get base unit + Custom { // for temperature + to_base: fn(f64) -> f64, + from_base: fn(f64) -> f64, + }, +} + +enum Category { + Temperature, + Length, + Weight, + Volume, + Speed, + Area, + Data, + Time, + Pressure, + Energy, + Currency, +} +``` + +### Base units per category + +| Category | Base unit | Rationale | +|----------|-----------|-----------| +| Temperature | Kelvin | Standard SI, no negative zero issues | +| Length | Meter | SI | +| Weight | Kilogram | SI | +| Volume | Liter | Practical | +| Speed | m/s | SI | +| Area | m² | SI | +| Data | Byte | Fundamental | +| Time | Second | SI | +| Pressure | Pascal | SI | +| Energy | Joule | SI | +| Currency | EUR | ECB reports rates relative to EUR | + +### Temperature conversions (special case) + +``` +Celsius → Kelvin: K = C + 273.15 +Fahrenheit → Kelvin: K = (F - 32) * 5/9 + 273.15 +Kelvin → Celsius: C = K - 273.15 +Kelvin → Fahrenheit: F = (K - 273.15) * 9/5 + 32 +``` + +--- + +## 5. Unit Table + +### Temperature +| Unit | Symbol | Aliases | Factor/Conversion | +|------|--------|---------|-------------------| +| Celsius | °C | `c`, `°c`, `celsius`, `degc`, `centigrade` | Custom | +| Fahrenheit | °F | `f`, `°f`, `fahrenheit`, `degf` | Custom | +| Kelvin | K | `k`, `kelvin` | Base | + +### Length +| Unit | Symbol | Aliases | Factor (to meters) | +|------|--------|---------|-------------------| +| Millimeter | mm | `mm`, `millimeter`, `millimeters`, `millimetre` | 0.001 | +| Centimeter | cm | `cm`, `centimeter`, `centimeters`, `centimetre` | 0.01 | +| Meter | m | `m`, `meter`, `meters`, `metre`, `metres` | 1.0 (base) | +| Kilometer | km | `km`, `kms`, `kilometer`, `kilometers`, `kilometre` | 1000.0 | +| Inch | in | `in`, `inch`, `inches`, `"` | 0.0254 | +| Foot | ft | `ft`, `foot`, `feet`, `'` | 0.3048 | +| Yard | yd | `yd`, `yard`, `yards` | 0.9144 | +| Mile | mi | `mi`, `mile`, `miles` | 1609.344 | +| Nautical mile | nmi | `nmi`, `nautical_mile`, `nautical_miles` | 1852.0 | + +### Weight/Mass +| Unit | Symbol | Aliases | Factor (to kg) | +|------|--------|---------|----------------| +| Milligram | mg | `mg`, `milligram`, `milligrams` | 0.000001 | +| Gram | g | `g`, `gram`, `grams` | 0.001 | +| Kilogram | kg | `kg`, `kilogram`, `kilograms`, `kilo`, `kilos` | 1.0 (base) | +| Metric ton | t | `t`, `ton`, `tons`, `tonne`, `tonnes`, `metric_ton` | 1000.0 | +| US short ton | short_ton | `short_ton`, `ton_us` | 907.185 | +| Ounce | oz | `oz`, `ounce`, `ounces` | 0.0283495 | +| Pound | lb | `lb`, `lbs`, `pound`, `pounds` | 0.453592 | +| Stone | st | `st`, `stone`, `stones` | 6.35029 | + +### Volume +| Unit | Symbol | Aliases | Factor (to liters) | +|------|--------|---------|-------------------| +| Milliliter | ml | `ml`, `milliliter`, `milliliters`, `millilitre` | 0.001 | +| Liter | l | `l`, `liter`, `liters`, `litre`, `litres` | 1.0 (base) | +| US gallon | gal | `gal`, `gallon`, `gallons` | 3.78541 | +| Imperial gallon | imp_gal | `imp_gal`, `gal_uk`, `imperial_gallon` | 4.54609 | +| Quart | qt | `qt`, `quart`, `quarts` | 0.946353 | +| Pint | pt | `pt`, `pint`, `pints` | 0.473176 | +| Cup | cup | `cup`, `cups` | 0.236588 | +| Fluid ounce | fl oz | `floz`, `fl_oz`, `fluid_ounce`, `fluid_ounces` | 0.0295735 | +| Tablespoon | tbsp | `tbsp`, `tablespoon`, `tablespoons` | 0.0147868 | +| Teaspoon | tsp | `tsp`, `teaspoon`, `teaspoons` | 0.00492892 | + +### Speed +| Unit | Symbol | Aliases | Factor (to m/s) | +|------|--------|---------|-----------------| +| m/s | m/s | `m/s`, `mps`, `meters_per_second` | 1.0 (base) | +| km/h | km/h | `km/h`, `kmh`, `kph`, `kilometers_per_hour` | 0.277778 | +| mph | mph | `mph`, `miles_per_hour` | 0.44704 | +| Knot | kn | `kn`, `kt`, `knot`, `knots` | 0.514444 | +| ft/s | ft/s | `ft/s`, `fps`, `feet_per_second` | 0.3048 | + +### Area +| Unit | Symbol | Aliases | Factor (to m²) | +|------|--------|---------|----------------| +| mm² | mm² | `mm2`, `sqmm`, `square_millimeter` | 0.000001 | +| cm² | cm² | `cm2`, `sqcm`, `square_centimeter` | 0.0001 | +| m² | m² | `m2`, `sqm`, `square_meter`, `square_meters` | 1.0 (base) | +| km² | km² | `km2`, `sqkm`, `square_kilometer` | 1000000.0 | +| ft² | ft² | `ft2`, `sqft`, `square_foot`, `square_feet` | 0.092903 | +| Acre | ac | `ac`, `acre`, `acres` | 4046.86 | +| Hectare | ha | `ha`, `hectare`, `hectares` | 10000.0 | + +### Data +| Unit | Symbol | Aliases | Factor (to bytes) | +|------|--------|---------|-------------------| +| Byte | B | `b`, `byte`, `bytes` | 1.0 (base) | +| Kilobyte | KB | `kb`, `kilobyte`, `kilobytes` | 1000.0 | +| Megabyte | MB | `mb`, `megabyte`, `megabytes` | 1000000.0 | +| Gigabyte | GB | `gb`, `gigabyte`, `gigabytes` | 1000000000.0 | +| Terabyte | TB | `tb`, `terabyte`, `terabytes` | 1000000000000.0 | +| Kibibyte | KiB | `kib`, `kibibyte`, `kibibytes` | 1024.0 | +| Mebibyte | MiB | `mib`, `mebibyte`, `mebibytes` | 1048576.0 | +| Gibibyte | GiB | `gib`, `gibibyte`, `gibibytes` | 1073741824.0 | +| Tebibyte | TiB | `tib`, `tebibyte`, `tebibytes` | 1099511627776.0 | + +### Time +| Unit | Symbol | Aliases | Factor (to seconds) | +|------|--------|---------|---------------------| +| Second | s | `s`, `sec`, `second`, `seconds` | 1.0 (base) | +| Minute | min | `min`, `minute`, `minutes` | 60.0 | +| Hour | h | `h`, `hr`, `hour`, `hours` | 3600.0 | +| Day | d | `d`, `day`, `days` | 86400.0 | +| Week | wk | `wk`, `week`, `weeks` | 604800.0 | +| Month | mo | `mo`, `month`, `months` | 2592000.0 (30 days) | +| Year | yr | `yr`, `year`, `years` | 31536000.0 (365 days) | + +### Pressure +| Unit | Symbol | Aliases | Factor (to Pa) | +|------|--------|---------|----------------| +| Pascal | Pa | `pa`, `pascal`, `pascals` | 1.0 (base) | +| Hectopascal | hPa | `hpa`, `hectopascal` | 100.0 | +| Kilopascal | kPa | `kpa`, `kilopascal` | 1000.0 | +| Bar | bar | `bar`, `bars` | 100000.0 | +| Millibar | mbar | `mbar`, `millibar` | 100.0 | +| PSI | psi | `psi`, `pounds_per_square_inch` | 6894.76 | +| Atmosphere | atm | `atm`, `atmosphere`, `atmospheres` | 101325.0 | +| mmHg | mmHg | `mmhg`, `torr` | 133.322 | + +### Energy +| Unit | Symbol | Aliases | Factor (to Joules) | +|------|--------|---------|-------------------| +| Joule | J | `j`, `joule`, `joules` | 1.0 (base) | +| Kilojoule | kJ | `kj`, `kilojoule`, `kilojoules` | 1000.0 | +| Calorie | cal | `cal`, `calorie`, `calories` | 4.184 | +| Kilocalorie | kcal | `kcal`, `kilocalorie`, `kilocalories` | 4184.0 | +| Watt-hour | Wh | `wh`, `watt_hour` | 3600.0 | +| Kilowatt-hour | kWh | `kwh`, `kilowatt_hour` | 3600000.0 | +| BTU | BTU | `btu`, `british_thermal_unit` | 1055.06 | + +--- + +## 6. Currency + +### Data source + +ECB daily reference rates: `https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml` + +Returns ~30 currencies with rates relative to EUR. + +### Caching + +- Cache file: `~/.cache/owlry/ecb_rates.json` +- Format: `{ "date": "2026-03-26", "rates": { "USD": 1.0832, "GBP": 0.8345, ... } }` +- Refresh when cache is older than 24 hours +- If fetch fails, use cached rates silently (stale rates > no rates) +- If no cache exists and fetch fails, currency conversions return no results + +### Currency aliases + +Each currency accepts ISO code, lowercase, full name, and common symbol: + +| Currency | Aliases | +|----------|---------| +| EUR | `eur`, `euro`, `euros`, `€` | +| USD | `usd`, `dollar`, `dollars`, `$`, `us_dollar` | +| GBP | `gbp`, `pound_sterling`, `£`, `british_pound` | +| JPY | `jpy`, `yen`, `¥`, `japanese_yen` | +| CHF | `chf`, `swiss_franc`, `francs` | +| CAD | `cad`, `canadian_dollar`, `c$` | +| AUD | `aud`, `australian_dollar`, `a$` | +| CNY | `cny`, `yuan`, `renminbi`, `rmb` | +| SEK | `sek`, `swedish_krona`, `kronor` | +| NOK | `nok`, `norwegian_krone` | +| DKK | `dkk`, `danish_krone` | +| PLN | `pln`, `zloty`, `złoty` | +| CZK | `czk`, `czech_koruna` | +| HUF | `huf`, `forint` | +| TRY | `try`, `turkish_lira`, `lira` | +| (others) | ISO code + lowercase | + +### Conversion + +EUR is the base. To convert A→B: `result = value / rate_A * rate_B` + +(EUR rate is implicitly 1.0 since ECB reports everything relative to EUR.) + +--- + +## 7. Result Display + +### With target unit specified + +One result: + +``` +Name: 37.78 °C +Description: 100 °F = 37.78 °C +Icon: edit-find-replace +Command: (copy "37.78" to clipboard via wl-copy) +``` + +### Without target unit + +Up to 5 results showing common conversions for that category: + +``` +Query: "100 F" + +Result 1: 37.78 °C │ 100 °F = 37.78 °C +Result 2: 310.93 K │ 100 °F = 310.93 K +``` + +``` +Query: "100 km" + +Result 1: 62.14 mi │ 100 km = 62.14 mi +Result 2: 100000 m │ 100 km = 100,000 m +Result 3: 328084 ft │ 100 km = 328,084 ft +Result 4: 109361 yd │ 100 km = 109,361 yd +``` + +### Common conversions per category + +| Category | Show (excluding source unit) | +|----------|------------------------------| +| Temperature | °C, °F, K | +| Length | m, km, ft, mi, in | +| Weight | kg, lb, oz, g, st | +| Volume | l, gal, ml, cup, fl oz | +| Speed | km/h, mph, m/s, kn | +| Area | m², ft², acres, ha, km² | +| Data | MB, GB, MiB, GiB, TB | +| Time | s, min, h, days, weeks | +| Pressure | bar, psi, atm, hPa, mmHg | +| Energy | kJ, kcal, kWh, BTU, Wh | +| Currency | USD, EUR, GBP, JPY, CNY | + +### Number formatting + +- Up to 4 decimal places, trailing zeros stripped +- Large numbers get thousand separators: `1,000,000` +- Very small numbers use scientific notation if < 0.0001 +- Currency always shows 2 decimal places + +--- + +## 8. File Structure + +``` +owlry-plugins/crates/owlry-plugin-converter/ +├── Cargo.toml +└── src/ + ├── lib.rs # Plugin interface (vtable, init, query dispatch) + ├── parser.rs # Query parsing (number + unit extraction) + ├── units.rs # Unit definitions, conversion logic, category tables + └── currency.rs # ECB fetch, cache, currency-specific handling +``` + +### Dependencies + +```toml +[dependencies] +owlry-plugin-api = { git = "https://somegit.dev/Owlibou/owlry.git", tag = "plugin-api-v1.0.0" } +abi_stable = "0.11" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +reqwest = { version = "0.13", features = ["blocking"] } +dirs = "5" +``` + +--- + +## 9. Out of Scope + +- Cryptocurrency (future addition) +- Compound units (`km/h² to m/s²`) +- Math expressions in conversion (`2 * 50 kg to lb`) +- Configurable default targets per category +- Locale-aware number formatting (use `.` as decimal separator always) +- Offline currency rate bundling (rely on cache)