docs: add converter plugin design spec

This commit is contained in:
2026-03-26 15:07:04 +01:00
parent d8d26f4fd2
commit 268fd49741

View File

@@ -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)