- 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)
238 lines
6.2 KiB
Rust
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());
|
|
}
|
|
}
|