docs: add converter plugin design spec
This commit is contained in:
419
docs/superpowers/specs/2026-03-26-converter-plugin-design.md
Normal file
419
docs/superpowers/specs/2026-03-26-converter-plugin-design.md
Normal 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)
|
||||
Reference in New Issue
Block a user