Files
owlry-plugins/crates/owlry-plugin-converter/src/lib.rs
vikingowl c73f57578d fix(converter): fix double unit in description, broken icon, currency aliases
- Description showed "20 m = 0.02 km km" — display_value already
  includes the unit symbol, removed redundant r.target_symbol
- Icon changed from "edit-find-replace" to "edit-find-replace-symbolic"
  which exists in Adwaita
- Currency aliases (dollar, euro, etc.) now resolve in convert_to and
  convert_common — they were only handled by find_unit (parser validation)
  but not by lookup_unit (actual conversion)
2026-03-28 10:23:49 +01:00

238 lines
6.2 KiB
Rust

//! Converter Plugin for Owlry
//!
//! A dynamic provider that converts between units and currencies.
//! Supports queries prefixed with `>` or auto-detected.
//!
//! Examples:
//! - `> 100 F to C` → 37.78 °C
//! - `50 kg in lb` → 110.23 lb
//! - `100 eur to usd` → 108.32 USD
mod currency;
mod parser;
mod units;
use abi_stable::std_types::{ROption, RStr, RString, RVec};
use owlry_plugin_api::{
API_VERSION, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind,
ProviderPosition, owlry_plugin,
};
const PLUGIN_ID: &str = "converter";
const PLUGIN_NAME: &str = "Converter";
const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION");
const PLUGIN_DESCRIPTION: &str = "Convert between units and currencies";
const PROVIDER_ID: &str = "converter";
const PROVIDER_NAME: &str = "Converter";
const PROVIDER_PREFIX: &str = ">";
const PROVIDER_ICON: &str = "edit-find-replace-symbolic";
const PROVIDER_TYPE_ID: &str = "conv";
struct ConverterState;
extern "C" fn plugin_info() -> PluginInfo {
PluginInfo {
id: RString::from(PLUGIN_ID),
name: RString::from(PLUGIN_NAME),
version: RString::from(PLUGIN_VERSION),
description: RString::from(PLUGIN_DESCRIPTION),
api_version: API_VERSION,
}
}
extern "C" fn plugin_providers() -> RVec<ProviderInfo> {
vec![ProviderInfo {
id: RString::from(PROVIDER_ID),
name: RString::from(PROVIDER_NAME),
prefix: ROption::RSome(RString::from(PROVIDER_PREFIX)),
icon: RString::from(PROVIDER_ICON),
provider_type: ProviderKind::Dynamic,
type_id: RString::from(PROVIDER_TYPE_ID),
position: ProviderPosition::Normal,
priority: 9000,
}]
.into()
}
extern "C" fn provider_init(_provider_id: RStr<'_>) -> ProviderHandle {
let state = Box::new(ConverterState);
ProviderHandle::from_box(state)
}
extern "C" fn provider_refresh(_handle: ProviderHandle) -> RVec<PluginItem> {
RVec::new()
}
extern "C" fn provider_query(_handle: ProviderHandle, query: RStr<'_>) -> RVec<PluginItem> {
let query_str = query.as_str().trim();
// Strip prefix
let input = if let Some(rest) = query_str.strip_prefix('>') {
rest.trim()
} else {
query_str
};
let parsed = match parser::parse_conversion(input) {
Some(p) => p,
None => return RVec::new(),
};
let results = if let Some(ref target) = parsed.target_unit {
units::convert_to(&parsed.value, &parsed.from_unit, target)
.into_iter()
.collect()
} else {
units::convert_common(&parsed.value, &parsed.from_unit)
};
results
.into_iter()
.map(|r| {
PluginItem::new(
format!("conv:{}:{}:{}", parsed.from_unit, r.target_symbol, r.value),
r.display_value.clone(),
format!("printf '%s' '{}' | wl-copy", r.raw_value.replace('\'', "'\\''")),
)
.with_description(format!(
"{} {} = {}",
format_number(parsed.value),
parsed.from_symbol,
r.display_value,
))
.with_icon(PROVIDER_ICON)
})
.collect::<Vec<_>>()
.into()
}
extern "C" fn provider_drop(handle: ProviderHandle) {
if !handle.ptr.is_null() {
unsafe {
handle.drop_as::<ConverterState>();
}
}
}
owlry_plugin! {
info: plugin_info,
providers: plugin_providers,
init: provider_init,
refresh: provider_refresh,
query: provider_query,
drop: provider_drop,
}
fn format_number(n: f64) -> String {
if n.fract() == 0.0 && n.abs() < 1e15 {
let i = n as i64;
if i.abs() >= 1000 {
format_with_separators(i)
} else {
format!("{}", i)
}
} else {
format!("{:.4}", n)
.trim_end_matches('0')
.trim_end_matches('.')
.to_string()
}
}
fn format_with_separators(n: i64) -> String {
let s = n.abs().to_string();
let mut result = String::new();
for (i, c) in s.chars().rev().enumerate() {
if i > 0 && i % 3 == 0 {
result.push(',');
}
result.push(c);
}
if n < 0 {
result.push('-');
}
result.chars().rev().collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_format_number_integer() {
assert_eq!(format_number(42.0), "42");
}
#[test]
fn test_format_number_large_integer() {
assert_eq!(format_number(1000000.0), "1,000,000");
}
#[test]
fn test_format_number_decimal() {
assert_eq!(format_number(3.14), "3.14");
}
#[test]
fn test_format_with_separators() {
assert_eq!(format_with_separators(1234567), "1,234,567");
assert_eq!(format_with_separators(999), "999");
assert_eq!(format_with_separators(-1234), "-1,234");
}
#[test]
fn test_provider_query_with_prefix() {
let result = provider_query(
ProviderHandle {
ptr: std::ptr::null_mut(),
},
RStr::from("> 100 km to mi"),
);
assert!(!result.is_empty());
}
#[test]
fn test_provider_query_auto_detect() {
let result = provider_query(
ProviderHandle {
ptr: std::ptr::null_mut(),
},
RStr::from("100 km to mi"),
);
assert!(!result.is_empty());
}
#[test]
fn test_provider_query_no_target() {
let result = provider_query(
ProviderHandle {
ptr: std::ptr::null_mut(),
},
RStr::from("> 100 km"),
);
assert!(result.len() > 1);
}
#[test]
fn test_provider_query_nonsense() {
let result = provider_query(
ProviderHandle {
ptr: std::ptr::null_mut(),
},
RStr::from("hello world"),
);
assert!(result.is_empty());
}
#[test]
fn test_provider_query_temperature() {
let result = provider_query(
ProviderHandle {
ptr: std::ptr::null_mut(),
},
RStr::from("102F to C"),
);
assert!(!result.is_empty());
}
}