diff --git a/Cargo.lock b/Cargo.lock index ed3b5d1..9ef152d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -817,16 +817,6 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" -[[package]] -name = "meval" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" -dependencies = [ - "fnv", - "nom", -] - [[package]] name = "mime" version = "0.3.17" @@ -844,12 +834,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "nom" -version = "1.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" - [[package]] name = "once_cell" version = "1.21.4" @@ -889,15 +873,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "owlry-plugin-calculator" -version = "1.0.1" -dependencies = [ - "abi_stable", - "meval", - "owlry-plugin-api", -] - [[package]] name = "owlry-plugin-clipboard" version = "1.0.0" @@ -906,18 +881,6 @@ dependencies = [ "owlry-plugin-api", ] -[[package]] -name = "owlry-plugin-converter" -version = "1.0.2" -dependencies = [ - "abi_stable", - "dirs", - "owlry-plugin-api", - "reqwest", - "serde", - "serde_json", -] - [[package]] name = "owlry-plugin-emoji" version = "1.0.1" @@ -974,14 +937,6 @@ dependencies = [ "toml", ] -[[package]] -name = "owlry-plugin-system" -version = "1.0.0" -dependencies = [ - "abi_stable", - "owlry-plugin-api", -] - [[package]] name = "owlry-plugin-systemd" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index fe8ef21..72d2d54 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,6 @@ [workspace] members = [ "crates/owlry-plugin-bookmarks", - "crates/owlry-plugin-calculator", - "crates/owlry-plugin-converter", "crates/owlry-plugin-clipboard", "crates/owlry-plugin-emoji", "crates/owlry-plugin-filesearch", @@ -10,7 +8,6 @@ members = [ "crates/owlry-plugin-pomodoro", "crates/owlry-plugin-scripts", "crates/owlry-plugin-ssh", - "crates/owlry-plugin-system", "crates/owlry-plugin-systemd", "crates/owlry-plugin-weather", "crates/owlry-plugin-websearch", diff --git a/aur/owlry-plugin-converter/owlry-plugin-converter-1.0.0.tar.gz b/aur/owlry-plugin-converter/owlry-plugin-converter-1.0.0.tar.gz new file mode 100644 index 0000000..e9d4e63 Binary files /dev/null and b/aur/owlry-plugin-converter/owlry-plugin-converter-1.0.0.tar.gz differ diff --git a/crates/owlry-plugin-calculator/Cargo.toml b/crates/owlry-plugin-calculator/Cargo.toml deleted file mode 100644 index f4ace61..0000000 --- a/crates/owlry-plugin-calculator/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "owlry-plugin-calculator" -version = "1.0.1" -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -description = "Calculator plugin for owlry - evaluates mathematical expressions" -keywords = ["owlry", "plugin", "calculator"] -categories = ["mathematics"] - -[lib] -crate-type = ["cdylib"] # Compile as dynamic library (.so) - -[dependencies] -# Plugin API for owlry -owlry-plugin-api = { git = "https://somegit.dev/Owlibou/owlry.git", tag = "plugin-api-v1.0.0" } - -# Math expression evaluation -meval = "0.2" - -# ABI-stable types (re-exported from owlry-plugin-api, but needed for RString etc) -abi_stable = "0.11" diff --git a/crates/owlry-plugin-calculator/src/lib.rs b/crates/owlry-plugin-calculator/src/lib.rs deleted file mode 100644 index 7288fe7..0000000 --- a/crates/owlry-plugin-calculator/src/lib.rs +++ /dev/null @@ -1,231 +0,0 @@ -//! Calculator Plugin for Owlry -//! -//! A dynamic provider that evaluates mathematical expressions. -//! Supports queries prefixed with `=` or `calc `. -//! -//! Examples: -//! - `= 5 + 3` → 8 -//! - `calc sqrt(16)` → 4 -//! - `= pi * 2` → 6.283185... - -use abi_stable::std_types::{ROption, RStr, RString, RVec}; -use owlry_plugin_api::{ - API_VERSION, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, - ProviderPosition, owlry_plugin, -}; - -// Plugin metadata -const PLUGIN_ID: &str = "calculator"; -const PLUGIN_NAME: &str = "Calculator"; -const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION"); -const PLUGIN_DESCRIPTION: &str = "Evaluate mathematical expressions"; - -// Provider metadata -const PROVIDER_ID: &str = "calculator"; -const PROVIDER_NAME: &str = "Calculator"; -const PROVIDER_PREFIX: &str = "="; -const PROVIDER_ICON: &str = "accessories-calculator"; -const PROVIDER_TYPE_ID: &str = "calc"; - -/// Calculator provider state (empty for now, but could cache results) -struct CalculatorState; - -// ============================================================================ -// Plugin Interface Implementation -// ============================================================================ - -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 { - 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: 10000, // Dynamic: calculator results first - }] - .into() -} - -extern "C" fn provider_init(_provider_id: RStr<'_>) -> ProviderHandle { - // Create state and return handle - let state = Box::new(CalculatorState); - ProviderHandle::from_box(state) -} - -extern "C" fn provider_refresh(_handle: ProviderHandle) -> RVec { - // Dynamic provider - refresh does nothing - RVec::new() -} - -extern "C" fn provider_query(_handle: ProviderHandle, query: RStr<'_>) -> RVec { - let query_str = query.as_str(); - - // Extract expression from query - let expr = match extract_expression(query_str) { - Some(e) if !e.is_empty() => e, - _ => return RVec::new(), - }; - - // Evaluate the expression - match evaluate_expression(expr) { - Some(item) => vec![item].into(), - None => RVec::new(), - } -} - -extern "C" fn provider_drop(handle: ProviderHandle) { - if !handle.ptr.is_null() { - // SAFETY: We created this handle from Box - unsafe { - handle.drop_as::(); - } - } -} - -// Register the plugin vtable -owlry_plugin! { - info: plugin_info, - providers: plugin_providers, - init: provider_init, - refresh: provider_refresh, - query: provider_query, - drop: provider_drop, -} - -// ============================================================================ -// Calculator Logic -// ============================================================================ - -/// Extract expression from query (handles `= expr` and `calc expr` formats) -fn extract_expression(query: &str) -> Option<&str> { - let trimmed = query.trim(); - - // Support both "= expr" and "=expr" (with or without space) - if let Some(expr) = trimmed.strip_prefix("= ") { - Some(expr.trim()) - } else if let Some(expr) = trimmed.strip_prefix('=') { - Some(expr.trim()) - } else if let Some(expr) = trimmed.strip_prefix("calc ") { - Some(expr.trim()) - } else { - // For filter mode - accept raw expressions - Some(trimmed) - } -} - -/// Evaluate a mathematical expression and return a PluginItem -fn evaluate_expression(expr: &str) -> Option { - match meval::eval_str(expr) { - Ok(result) => { - // Format result nicely - let result_str = format_result(result); - - Some( - PluginItem::new( - format!("calc:{}", expr), - result_str.clone(), - format!("printf '%s' '{}' | wl-copy", result_str.replace('\'', "'\\''")), - ) - .with_description(format!("= {}", expr)) - .with_icon(PROVIDER_ICON) - .with_keywords(vec!["math".to_string(), "calculator".to_string()]), - ) - } - Err(_) => None, - } -} - -/// Format a numeric result nicely -fn format_result(result: f64) -> String { - if result.fract() == 0.0 && result.abs() < 1e15 { - // Integer result - format!("{}", result as i64) - } else { - // Float result with reasonable precision, trimming trailing zeros - let formatted = format!("{:.10}", result); - formatted - .trim_end_matches('0') - .trim_end_matches('.') - .to_string() - } -} - -// ============================================================================ -// Tests -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_extract_expression() { - assert_eq!(extract_expression("= 5+3"), Some("5+3")); - assert_eq!(extract_expression("=5+3"), Some("5+3")); - assert_eq!(extract_expression("calc 5+3"), Some("5+3")); - assert_eq!(extract_expression(" = 5 + 3 "), Some("5 + 3")); - assert_eq!(extract_expression("5+3"), Some("5+3")); // Raw expression - } - - #[test] - fn test_format_result() { - assert_eq!(format_result(8.0), "8"); - assert_eq!(format_result(2.5), "2.5"); - assert_eq!(format_result(3.14159265358979), "3.1415926536"); - } - - #[test] - fn test_evaluate_basic() { - let item = evaluate_expression("5+3").unwrap(); - assert_eq!(item.name.as_str(), "8"); - - let item = evaluate_expression("10 * 2").unwrap(); - assert_eq!(item.name.as_str(), "20"); - - let item = evaluate_expression("15 / 3").unwrap(); - assert_eq!(item.name.as_str(), "5"); - } - - #[test] - fn test_evaluate_float() { - let item = evaluate_expression("5/2").unwrap(); - assert_eq!(item.name.as_str(), "2.5"); - } - - #[test] - fn test_evaluate_functions() { - let item = evaluate_expression("sqrt(16)").unwrap(); - assert_eq!(item.name.as_str(), "4"); - - let item = evaluate_expression("abs(-5)").unwrap(); - assert_eq!(item.name.as_str(), "5"); - } - - #[test] - fn test_evaluate_constants() { - let item = evaluate_expression("pi").unwrap(); - assert!(item.name.as_str().starts_with("3.14159")); - - let item = evaluate_expression("e").unwrap(); - assert!(item.name.as_str().starts_with("2.718")); - } - - #[test] - fn test_evaluate_invalid() { - assert!(evaluate_expression("").is_none()); - assert!(evaluate_expression("invalid").is_none()); - assert!(evaluate_expression("5 +").is_none()); - } -} diff --git a/crates/owlry-plugin-converter/Cargo.toml b/crates/owlry-plugin-converter/Cargo.toml deleted file mode 100644 index 1fdadbe..0000000 --- a/crates/owlry-plugin-converter/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "owlry-plugin-converter" -version = "1.0.2" -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -description = "Unit and currency conversion plugin for owlry" -keywords = ["owlry", "plugin", "converter", "units", "currency"] -categories = ["science"] - -[lib] -crate-type = ["cdylib"] - -[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" diff --git a/crates/owlry-plugin-converter/src/currency.rs b/crates/owlry-plugin-converter/src/currency.rs deleted file mode 100644 index ea35c44..0000000 --- a/crates/owlry-plugin-converter/src/currency.rs +++ /dev/null @@ -1,313 +0,0 @@ -use std::collections::HashMap; -use std::fs; -use std::path::PathBuf; -use std::sync::Mutex; -use std::time::SystemTime; - -use serde::{Deserialize, Serialize}; - -const ECB_URL: &str = "https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml"; -const CACHE_MAX_AGE_SECS: u64 = 86400; // 24 hours - -static CACHED_RATES: Mutex> = Mutex::new(None); - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct CurrencyRates { - pub date: String, - pub rates: HashMap, -} - -struct CurrencyAlias { - code: &'static str, - aliases: &'static [&'static str], -} - -static CURRENCY_ALIASES: &[CurrencyAlias] = &[ - CurrencyAlias { - code: "EUR", - aliases: &["eur", "euro", "euros", "€"], - }, - CurrencyAlias { - code: "USD", - aliases: &["usd", "dollar", "dollars", "$", "us_dollar"], - }, - CurrencyAlias { - code: "GBP", - aliases: &["gbp", "pound_sterling", "£", "british_pound", "pounds"], - }, - CurrencyAlias { - code: "JPY", - aliases: &["jpy", "yen", "¥", "japanese_yen"], - }, - CurrencyAlias { - code: "CHF", - aliases: &["chf", "swiss_franc", "francs"], - }, - CurrencyAlias { - code: "CAD", - aliases: &["cad", "canadian_dollar", "c$"], - }, - CurrencyAlias { - code: "AUD", - aliases: &["aud", "australian_dollar", "a$"], - }, - CurrencyAlias { - code: "CNY", - aliases: &["cny", "yuan", "renminbi", "rmb"], - }, - CurrencyAlias { - code: "SEK", - aliases: &["sek", "swedish_krona", "kronor"], - }, - CurrencyAlias { - code: "NOK", - aliases: &["nok", "norwegian_krone"], - }, - CurrencyAlias { - code: "DKK", - aliases: &["dkk", "danish_krone"], - }, - CurrencyAlias { - code: "PLN", - aliases: &["pln", "zloty", "złoty"], - }, - CurrencyAlias { - code: "CZK", - aliases: &["czk", "czech_koruna"], - }, - CurrencyAlias { - code: "HUF", - aliases: &["huf", "forint"], - }, - CurrencyAlias { - code: "TRY", - aliases: &["try", "turkish_lira", "lira"], - }, -]; - -pub fn resolve_currency_code(alias: &str) -> Option<&'static str> { - let lower = alias.to_lowercase(); - - // Check aliases - for ca in CURRENCY_ALIASES { - if ca.aliases.contains(&lower.as_str()) { - return Some(ca.code); // ca.code is already &'static str - } - } - - // Check if it's a raw 3-letter ISO code we know about - let upper = alias.to_uppercase(); - if upper.len() == 3 { - if upper == "EUR" { - return Some("EUR"); - } - if let Some(rates) = get_rates() - && rates.rates.contains_key(&upper) - { - for ca in CURRENCY_ALIASES { - if ca.code == upper { - return Some(ca.code); - } - } - } - } - - None -} - -#[allow(dead_code)] -pub fn is_currency_alias(alias: &str) -> bool { - resolve_currency_code(alias).is_some() -} - -pub fn get_rates() -> Option { - // Check memory cache first - { - let cache = CACHED_RATES.lock().ok()?; - if let Some(ref rates) = *cache { - return Some(rates.clone()); - } - } - - // Try disk cache - if let Some(rates) = load_cache() - && !is_stale(&rates) - { - let mut cache = CACHED_RATES.lock().ok()?; - *cache = Some(rates.clone()); - return Some(rates); - } - - // Fetch fresh rates - if let Some(rates) = fetch_rates() { - save_cache(&rates); - let mut cache = CACHED_RATES.lock().ok()?; - *cache = Some(rates.clone()); - return Some(rates); - } - - // Fall back to stale cache - load_cache() -} - -fn cache_path() -> Option { - let cache_dir = dirs::cache_dir()?.join("owlry"); - Some(cache_dir.join("ecb_rates.json")) -} - -fn load_cache() -> Option { - let path = cache_path()?; - let content = fs::read_to_string(path).ok()?; - serde_json::from_str(&content).ok() -} - -fn save_cache(rates: &CurrencyRates) { - if let Some(path) = cache_path() { - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).ok(); - } - if let Ok(json) = serde_json::to_string_pretty(rates) { - fs::write(path, json).ok(); - } - } -} - -fn is_stale(_rates: &CurrencyRates) -> bool { - let path = match cache_path() { - Some(p) => p, - None => return true, - }; - let metadata = match fs::metadata(path) { - Ok(m) => m, - Err(_) => return true, - }; - let modified = match metadata.modified() { - Ok(t) => t, - Err(_) => return true, - }; - match SystemTime::now().duration_since(modified) { - Ok(age) => age.as_secs() > CACHE_MAX_AGE_SECS, - Err(_) => true, - } -} - -fn fetch_rates() -> Option { - let response = reqwest::blocking::get(ECB_URL).ok()?; - let body = response.text().ok()?; - parse_ecb_xml(&body) -} - -fn parse_ecb_xml(xml: &str) -> Option { - let mut rates = HashMap::new(); - let mut date = String::new(); - - for line in xml.lines() { - let trimmed = line.trim(); - - // Extract date: - if trimmed.contains("time=") - && let Some(start) = trimmed.find("time='") - { - let rest = &trimmed[start + 6..]; - if let Some(end) = rest.find('\'') { - date = rest[..end].to_string(); - } - } - - // Extract rate: - if trimmed.contains("currency=") && trimmed.contains("rate=") { - let currency = extract_attr(trimmed, "currency")?; - let rate_str = extract_attr(trimmed, "rate")?; - let rate: f64 = rate_str.parse().ok()?; - rates.insert(currency, rate); - } - } - - if rates.is_empty() { - return None; - } - - Some(CurrencyRates { date, rates }) -} - -fn extract_attr(line: &str, attr: &str) -> Option { - let needle = format!("{}='", attr); - let start = line.find(&needle)? + needle.len(); - let rest = &line[start..]; - let end = rest.find('\'')?; - Some(rest[..end].to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_resolve_currency_code_iso() { - assert_eq!(resolve_currency_code("usd"), Some("USD")); - assert_eq!(resolve_currency_code("EUR"), Some("EUR")); - } - - #[test] - fn test_resolve_currency_code_name() { - assert_eq!(resolve_currency_code("dollar"), Some("USD")); - assert_eq!(resolve_currency_code("euro"), Some("EUR")); - assert_eq!(resolve_currency_code("pounds"), Some("GBP")); - } - - #[test] - fn test_resolve_currency_code_symbol() { - assert_eq!(resolve_currency_code("$"), Some("USD")); - assert_eq!(resolve_currency_code("€"), Some("EUR")); - assert_eq!(resolve_currency_code("£"), Some("GBP")); - } - - #[test] - fn test_resolve_currency_unknown() { - assert_eq!(resolve_currency_code("xyz"), None); - } - - #[test] - fn test_is_currency_alias() { - assert!(is_currency_alias("usd")); - assert!(is_currency_alias("euro")); - assert!(is_currency_alias("$")); - assert!(!is_currency_alias("km")); - } - - #[test] - fn test_parse_ecb_xml() { - let xml = r#" - - Reference rates - - - - - - - -"#; - - let rates = parse_ecb_xml(xml).unwrap(); - assert!((rates.rates["USD"] - 1.0832).abs() < 0.001); - assert!((rates.rates["GBP"] - 0.8345).abs() < 0.001); - assert!((rates.rates["JPY"] - 161.94).abs() < 0.01); - } - - #[test] - fn test_cache_roundtrip() { - let rates = CurrencyRates { - date: "2026-03-26".to_string(), - rates: { - let mut m = HashMap::new(); - m.insert("USD".to_string(), 1.0832); - m.insert("GBP".to_string(), 0.8345); - m - }, - }; - let json = serde_json::to_string(&rates).unwrap(); - let parsed: CurrencyRates = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.rates["USD"], 1.0832); - } -} diff --git a/crates/owlry-plugin-converter/src/lib.rs b/crates/owlry-plugin-converter/src/lib.rs deleted file mode 100644 index 554f896..0000000 --- a/crates/owlry-plugin-converter/src/lib.rs +++ /dev/null @@ -1,237 +0,0 @@ -//! 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 { - 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 { - RVec::new() -} - -extern "C" fn provider_query(_handle: ProviderHandle, query: RStr<'_>) -> RVec { - 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::>() - .into() -} - -extern "C" fn provider_drop(handle: ProviderHandle) { - if !handle.ptr.is_null() { - unsafe { - handle.drop_as::(); - } - } -} - -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()); - } -} diff --git a/crates/owlry-plugin-converter/src/parser.rs b/crates/owlry-plugin-converter/src/parser.rs deleted file mode 100644 index 952d797..0000000 --- a/crates/owlry-plugin-converter/src/parser.rs +++ /dev/null @@ -1,235 +0,0 @@ -use crate::units; - -pub struct ParsedQuery { - pub value: f64, - pub from_unit: String, - pub from_symbol: String, - pub target_unit: Option, -} - -pub fn parse_conversion(input: &str) -> Option { - let input = input.trim(); - if input.is_empty() { - return None; - } - - // Extract leading number - let (value, rest) = extract_number(input)?; - let rest = rest.trim(); - - if rest.is_empty() { - return None; - } - - // Split on " to " or " in " (case-insensitive) - let (from_str, target_str) = split_on_connector(rest); - - // Resolve from unit - let from_lower = from_str.trim().to_lowercase(); - let from_symbol = units::find_unit(&from_lower)?; - - let from_symbol_str = from_symbol.to_string(); - - // Resolve target unit if present - let target_unit = target_str.and_then(|t| { - let t_lower = t.trim().to_lowercase(); - if t_lower.is_empty() { - None - } else { - units::find_unit(&t_lower).map(|_| t_lower) - } - }); - - Some(ParsedQuery { - value, - from_unit: from_lower, - from_symbol: from_symbol_str, - target_unit, - }) -} - -fn extract_number(input: &str) -> Option<(f64, &str)> { - let bytes = input.as_bytes(); - let mut i = 0; - - // Optional negative sign - if i < bytes.len() && bytes[i] == b'-' { - i += 1; - } - - // Must have at least one digit or start with . - if i >= bytes.len() { - return None; - } - - let start_digits = i; - - // Integer part - while i < bytes.len() && bytes[i].is_ascii_digit() { - i += 1; - } - - // Decimal part - if i < bytes.len() && bytes[i] == b'.' { - i += 1; - while i < bytes.len() && bytes[i].is_ascii_digit() { - i += 1; - } - } - - if i == start_digits && !(i > 0 && bytes[0] == b'-') { - // No digits found (and not just a negative sign before a dot) - // Handle ".5" case - if bytes[start_digits] == b'.' { - // already advanced past dot above - } else { - return None; - } - } - - if i == 0 || (i == 1 && bytes[0] == b'-') { - return None; - } - - let num_str = &input[..i]; - let value: f64 = num_str.parse().ok()?; - let rest = &input[i..]; - - Some((value, rest)) -} - -fn split_on_connector(input: &str) -> (&str, Option<&str>) { - let lower = input.to_lowercase(); - - // Try " to " first - if let Some(pos) = lower.find(" to ") { - let from = &input[..pos]; - let target = &input[pos + 4..]; - return (from, Some(target)); - } - - // Try " in " - if let Some(pos) = lower.find(" in ") { - let from = &input[..pos]; - let target = &input[pos + 4..]; - return (from, Some(target)); - } - - (input, None) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_number_and_unit_with_space() { - let p = parse_conversion("100 km").unwrap(); - assert!((p.value - 100.0).abs() < 0.001); - assert_eq!(p.from_unit, "km"); - assert!(p.target_unit.is_none()); - } - - #[test] - fn test_number_and_unit_no_space() { - let p = parse_conversion("100km").unwrap(); - assert!((p.value - 100.0).abs() < 0.001); - assert_eq!(p.from_unit, "km"); - } - - #[test] - fn test_with_target_to() { - let p = parse_conversion("100 km to mi").unwrap(); - assert!((p.value - 100.0).abs() < 0.001); - assert_eq!(p.from_unit, "km"); - assert_eq!(p.target_unit.as_deref(), Some("mi")); - } - - #[test] - fn test_with_target_in() { - let p = parse_conversion("100 km in mi").unwrap(); - assert_eq!(p.target_unit.as_deref(), Some("mi")); - } - - #[test] - fn test_temperature_no_space() { - let p = parse_conversion("102F to C").unwrap(); - assert!((p.value - 102.0).abs() < 0.001); - assert_eq!(p.from_unit, "f"); - assert_eq!(p.target_unit.as_deref(), Some("c")); - } - - #[test] - fn test_temperature_with_space() { - let p = parse_conversion("102 F in K").unwrap(); - assert!((p.value - 102.0).abs() < 0.001); - assert_eq!(p.from_unit, "f"); - assert_eq!(p.target_unit.as_deref(), Some("k")); - } - - #[test] - fn test_decimal_number() { - let p = parse_conversion("3.5 kg to lb").unwrap(); - assert!((p.value - 3.5).abs() < 0.001); - } - - #[test] - fn test_decimal_starting_with_dot() { - let p = parse_conversion(".5 kg").unwrap(); - assert!((p.value - 0.5).abs() < 0.001); - } - - #[test] - fn test_full_unit_names() { - let p = parse_conversion("100 kilometers to miles").unwrap(); - assert_eq!(p.from_unit, "kilometers"); - assert_eq!(p.target_unit.as_deref(), Some("miles")); - } - - #[test] - fn test_case_insensitive() { - let p = parse_conversion("100 KM TO MI").unwrap(); - assert_eq!(p.from_unit, "km"); - assert_eq!(p.target_unit.as_deref(), Some("mi")); - } - - #[test] - fn test_currency() { - let p = parse_conversion("100 eur to usd").unwrap(); - assert_eq!(p.from_unit, "eur"); - assert_eq!(p.target_unit.as_deref(), Some("usd")); - } - - #[test] - fn test_no_number_returns_none() { - assert!(parse_conversion("km to mi").is_none()); - } - - #[test] - fn test_unknown_unit_returns_none() { - assert!(parse_conversion("100 xyz to abc").is_none()); - } - - #[test] - fn test_empty_returns_none() { - assert!(parse_conversion("").is_none()); - } - - #[test] - fn test_number_only_returns_none() { - assert!(parse_conversion("100").is_none()); - } - - #[test] - fn test_compound_unit_alias() { - let p = parse_conversion("100 km/h to mph").unwrap(); - assert_eq!(p.from_unit, "km/h"); - assert_eq!(p.target_unit.as_deref(), Some("mph")); - } - - #[test] - fn test_multi_word_unit() { - let p = parse_conversion("100 fl_oz to ml").unwrap(); - assert_eq!(p.from_unit, "fl_oz"); - } -} diff --git a/crates/owlry-plugin-converter/src/units.rs b/crates/owlry-plugin-converter/src/units.rs deleted file mode 100644 index f43a085..0000000 --- a/crates/owlry-plugin-converter/src/units.rs +++ /dev/null @@ -1,944 +0,0 @@ -use std::collections::HashMap; -use std::sync::LazyLock; - -use crate::currency; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Category { - Temperature, - Length, - Weight, - Volume, - Speed, - Area, - Data, - Time, - Pressure, - Energy, - Currency, -} - -#[derive(Clone)] -enum Conversion { - Factor(f64), - Custom { - to_base: fn(f64) -> f64, - from_base: fn(f64) -> f64, - }, -} - -#[derive(Clone)] -pub(crate) struct UnitDef { - _id: &'static str, - symbol: &'static str, - aliases: &'static [&'static str], - category: Category, - conversion: Conversion, -} - -impl UnitDef { - fn to_base(&self, value: f64) -> f64 { - match &self.conversion { - Conversion::Factor(f) => value * f, - Conversion::Custom { to_base, .. } => to_base(value), - } - } - - fn convert_from_base(&self, value: f64) -> f64 { - match &self.conversion { - Conversion::Factor(f) => value / f, - Conversion::Custom { from_base, .. } => from_base(value), - } - } -} - -pub struct ConversionResult { - pub value: f64, - pub raw_value: String, - pub display_value: String, - pub target_symbol: String, -} - -static UNITS: LazyLock> = LazyLock::new(build_unit_table); -static ALIAS_MAP: LazyLock> = LazyLock::new(|| { - let mut map = HashMap::new(); - for (i, unit) in UNITS.iter().enumerate() { - for alias in unit.aliases { - map.insert(alias.to_lowercase(), i); - } - } - map -}); - -// Common conversions per category (symbols to show when no target specified) -static COMMON_TARGETS: LazyLock>> = LazyLock::new(|| { - let mut m = HashMap::new(); - m.insert(Category::Temperature, vec!["°C", "°F", "K"]); - m.insert(Category::Length, vec!["m", "km", "ft", "mi", "in"]); - m.insert(Category::Weight, vec!["kg", "lb", "oz", "g", "st"]); - m.insert(Category::Volume, vec!["l", "gal", "ml", "cup", "fl oz"]); - m.insert(Category::Speed, vec!["km/h", "mph", "m/s", "kn"]); - m.insert(Category::Area, vec!["m²", "ft²", "ac", "ha", "km²"]); - m.insert(Category::Data, vec!["MB", "GB", "MiB", "GiB", "TB"]); - m.insert(Category::Time, vec!["s", "min", "h", "d", "wk"]); - m.insert(Category::Pressure, vec!["bar", "psi", "atm", "hPa", "mmHg"]); - m.insert(Category::Energy, vec!["kJ", "kcal", "kWh", "BTU", "Wh"]); - m.insert(Category::Currency, vec!["USD", "EUR", "GBP", "JPY", "CNY"]); - m -}); - -pub fn find_unit(alias: &str) -> Option<&'static str> { - let lower = alias.to_lowercase(); - if let Some(&i) = ALIAS_MAP.get(&lower) { - return Some(UNITS[i].symbol); - } - currency::resolve_currency_code(&lower) -} - -pub fn lookup_unit(alias: &str) -> Option<(usize, &UnitDef)> { - let lower = alias.to_lowercase(); - ALIAS_MAP.get(&lower).map(|&i| (i, &UNITS[i])) -} - -pub fn convert_to(value: &f64, from: &str, to: &str) -> Option { - // Try currency first — currency aliases (dollar, euro, etc.) aren't in the UNITS table - if currency::is_currency_alias(from) || currency::is_currency_alias(to) { - return convert_currency(*value, from, to); - } - - let (_, from_def) = lookup_unit(from)?; - let (_, to_def) = lookup_unit(to)?; - - // Currency via UNITS table (shouldn't reach here, but just in case) - if from_def.category == Category::Currency || to_def.category == Category::Currency { - return convert_currency(*value, from, to); - } - - // Must be same category - if from_def.category != to_def.category { - return None; - } - - let base_value = from_def.to_base(*value); - let result = to_def.convert_from_base(base_value); - - Some(format_result(result, to_def.symbol)) -} - -pub fn convert_common(value: &f64, from: &str) -> Vec { - // Try currency first — currency aliases (dollar, euro, etc.) aren't in the UNITS table - if currency::is_currency_alias(from) { - return convert_currency_common(*value, from); - } - - let (_, from_def) = match lookup_unit(from) { - Some(u) => u, - None => return vec![], - }; - - let category = from_def.category; - let from_symbol = from_def.symbol; - - if category == Category::Currency { - return convert_currency_common(*value, from); - } - - let targets = match COMMON_TARGETS.get(&category) { - Some(t) => t, - None => return vec![], - }; - - let base_value = from_def.to_base(*value); - - targets - .iter() - .filter(|&&sym| sym != from_symbol) - .filter_map(|&sym| { - let (_, to_def) = lookup_unit(sym)?; - let result = to_def.convert_from_base(base_value); - Some(format_result(result, to_def.symbol)) - }) - .take(5) - .collect() -} - -fn convert_currency(value: f64, from: &str, to: &str) -> Option { - let rates = currency::get_rates()?; - let from_code = currency::resolve_currency_code(from)?; - let to_code = currency::resolve_currency_code(to)?; - - let from_rate = if from_code == "EUR" { 1.0 } else { *rates.rates.get(from_code)? }; - let to_rate = if to_code == "EUR" { 1.0 } else { *rates.rates.get(to_code)? }; - - let result = value / from_rate * to_rate; - Some(format_currency_result(result, to_code)) -} - -fn convert_currency_common(value: f64, from: &str) -> Vec { - let rates = match currency::get_rates() { - Some(r) => r, - None => return vec![], - }; - let from_code = match currency::resolve_currency_code(from) { - Some(c) => c, - None => return vec![], - }; - - let targets = COMMON_TARGETS.get(&Category::Currency).unwrap(); - let from_rate = if from_code == "EUR" { - 1.0 - } else { - match rates.rates.get(from_code) { - Some(&r) => r, - None => return vec![], - } - }; - - targets - .iter() - .filter(|&&sym| sym != from_code) - .filter_map(|&sym| { - let to_rate = if sym == "EUR" { 1.0 } else { *rates.rates.get(sym)? }; - let result = value / from_rate * to_rate; - Some(format_currency_result(result, sym)) - }) - .take(5) - .collect() -} - -fn format_result(value: f64, symbol: &str) -> ConversionResult { - let raw = if value.fract() == 0.0 && value.abs() < 1e15 { - format!("{}", value as i64) - } else { - format!("{:.4}", value) - .trim_end_matches('0') - .trim_end_matches('.') - .to_string() - }; - - let display = if value.abs() >= 1000.0 && value.fract() == 0.0 && value.abs() < 1e15 { - crate::format_with_separators(value as i64) - } else { - raw.clone() - }; - - ConversionResult { - value, - raw_value: raw, - display_value: format!("{} {}", display, symbol), - target_symbol: symbol.to_string(), - } -} - -fn format_currency_result(value: f64, code: &str) -> ConversionResult { - let raw = format!("{:.2}", value); - let display = raw.clone(); - - ConversionResult { - value, - raw_value: raw, - display_value: format!("{} {}", display, code), - target_symbol: code.to_string(), - } -} - -fn build_unit_table() -> Vec { - vec![ - // Temperature (base: Kelvin) - UnitDef { - _id: "celsius", - symbol: "°C", - aliases: &["c", "°c", "celsius", "degc", "centigrade"], - category: Category::Temperature, - conversion: Conversion::Custom { - to_base: |v| v + 273.15, - from_base: |v| v - 273.15, - }, - }, - UnitDef { - _id: "fahrenheit", - symbol: "°F", - aliases: &["f", "°f", "fahrenheit", "degf"], - category: Category::Temperature, - conversion: Conversion::Custom { - to_base: |v| (v - 32.0) * 5.0 / 9.0 + 273.15, - from_base: |v| (v - 273.15) * 9.0 / 5.0 + 32.0, - }, - }, - UnitDef { - _id: "kelvin", - symbol: "K", - aliases: &["k", "kelvin"], - category: Category::Temperature, - conversion: Conversion::Factor(1.0), // base - }, - // Length (base: meter) - UnitDef { - _id: "millimeter", - symbol: "mm", - aliases: &["mm", "millimeter", "millimeters", "millimetre"], - category: Category::Length, - conversion: Conversion::Factor(0.001), - }, - UnitDef { - _id: "centimeter", - symbol: "cm", - aliases: &["cm", "centimeter", "centimeters", "centimetre"], - category: Category::Length, - conversion: Conversion::Factor(0.01), - }, - UnitDef { - _id: "meter", - symbol: "m", - aliases: &["m", "meter", "meters", "metre", "metres"], - category: Category::Length, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "kilometer", - symbol: "km", - aliases: &["km", "kms", "kilometer", "kilometers", "kilometre"], - category: Category::Length, - conversion: Conversion::Factor(1000.0), - }, - UnitDef { - _id: "inch", - symbol: "in", - aliases: &["in", "inch", "inches"], - category: Category::Length, - conversion: Conversion::Factor(0.0254), - }, - UnitDef { - _id: "foot", - symbol: "ft", - aliases: &["ft", "foot", "feet"], - category: Category::Length, - conversion: Conversion::Factor(0.3048), - }, - UnitDef { - _id: "yard", - symbol: "yd", - aliases: &["yd", "yard", "yards"], - category: Category::Length, - conversion: Conversion::Factor(0.9144), - }, - UnitDef { - _id: "mile", - symbol: "mi", - aliases: &["mi", "mile", "miles"], - category: Category::Length, - conversion: Conversion::Factor(1609.344), - }, - UnitDef { - _id: "nautical_mile", - symbol: "nmi", - aliases: &["nmi", "nautical_mile", "nautical_miles"], - category: Category::Length, - conversion: Conversion::Factor(1852.0), - }, - // Weight (base: kg) - UnitDef { - _id: "milligram", - symbol: "mg", - aliases: &["mg", "milligram", "milligrams"], - category: Category::Weight, - conversion: Conversion::Factor(0.000001), - }, - UnitDef { - _id: "gram", - symbol: "g", - aliases: &["g", "gram", "grams"], - category: Category::Weight, - conversion: Conversion::Factor(0.001), - }, - UnitDef { - _id: "kilogram", - symbol: "kg", - aliases: &["kg", "kilogram", "kilograms", "kilo", "kilos"], - category: Category::Weight, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "tonne", - symbol: "t", - aliases: &["t", "ton", "tons", "tonne", "tonnes", "metric_ton"], - category: Category::Weight, - conversion: Conversion::Factor(1000.0), - }, - UnitDef { - _id: "short_ton", - symbol: "short_ton", - aliases: &["short_ton", "ton_us"], - category: Category::Weight, - conversion: Conversion::Factor(907.185), - }, - UnitDef { - _id: "ounce", - symbol: "oz", - aliases: &["oz", "ounce", "ounces"], - category: Category::Weight, - conversion: Conversion::Factor(0.0283495), - }, - UnitDef { - _id: "pound", - symbol: "lb", - aliases: &["lb", "lbs", "pound", "pounds"], - category: Category::Weight, - conversion: Conversion::Factor(0.453592), - }, - UnitDef { - _id: "stone", - symbol: "st", - aliases: &["st", "stone", "stones"], - category: Category::Weight, - conversion: Conversion::Factor(6.35029), - }, - // Volume (base: liter) - UnitDef { - _id: "milliliter", - symbol: "ml", - aliases: &["ml", "milliliter", "milliliters", "millilitre"], - category: Category::Volume, - conversion: Conversion::Factor(0.001), - }, - UnitDef { - _id: "liter", - symbol: "l", - aliases: &["l", "liter", "liters", "litre", "litres"], - category: Category::Volume, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "us_gallon", - symbol: "gal", - aliases: &["gal", "gallon", "gallons"], - category: Category::Volume, - conversion: Conversion::Factor(3.78541), - }, - UnitDef { - _id: "imp_gallon", - symbol: "imp gal", - aliases: &["imp_gal", "gal_uk", "imperial_gallon"], - category: Category::Volume, - conversion: Conversion::Factor(4.54609), - }, - UnitDef { - _id: "quart", - symbol: "qt", - aliases: &["qt", "quart", "quarts"], - category: Category::Volume, - conversion: Conversion::Factor(0.946353), - }, - UnitDef { - _id: "pint", - symbol: "pt", - aliases: &["pt", "pint", "pints"], - category: Category::Volume, - conversion: Conversion::Factor(0.473176), - }, - UnitDef { - _id: "cup", - symbol: "cup", - aliases: &["cup", "cups"], - category: Category::Volume, - conversion: Conversion::Factor(0.236588), - }, - UnitDef { - _id: "fluid_ounce", - symbol: "fl oz", - aliases: &["floz", "fl_oz", "fluid_ounce", "fluid_ounces"], - category: Category::Volume, - conversion: Conversion::Factor(0.0295735), - }, - UnitDef { - _id: "tablespoon", - symbol: "tbsp", - aliases: &["tbsp", "tablespoon", "tablespoons"], - category: Category::Volume, - conversion: Conversion::Factor(0.0147868), - }, - UnitDef { - _id: "teaspoon", - symbol: "tsp", - aliases: &["tsp", "teaspoon", "teaspoons"], - category: Category::Volume, - conversion: Conversion::Factor(0.00492892), - }, - // Speed (base: m/s) - UnitDef { - _id: "mps", - symbol: "m/s", - aliases: &["m/s", "mps", "meters_per_second"], - category: Category::Speed, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "kmh", - symbol: "km/h", - aliases: &["km/h", "kmh", "kph", "kilometers_per_hour"], - category: Category::Speed, - conversion: Conversion::Factor(0.277778), - }, - UnitDef { - _id: "mph", - symbol: "mph", - aliases: &["mph", "miles_per_hour"], - category: Category::Speed, - conversion: Conversion::Factor(0.44704), - }, - UnitDef { - _id: "knot", - symbol: "kn", - aliases: &["kn", "kt", "knot", "knots"], - category: Category::Speed, - conversion: Conversion::Factor(0.514444), - }, - UnitDef { - _id: "fps", - symbol: "ft/s", - aliases: &["ft/s", "fps", "feet_per_second"], - category: Category::Speed, - conversion: Conversion::Factor(0.3048), - }, - // Area (base: m²) - UnitDef { - _id: "sqmm", - symbol: "mm²", - aliases: &["mm2", "sqmm", "square_millimeter"], - category: Category::Area, - conversion: Conversion::Factor(0.000001), - }, - UnitDef { - _id: "sqcm", - symbol: "cm²", - aliases: &["cm2", "sqcm", "square_centimeter"], - category: Category::Area, - conversion: Conversion::Factor(0.0001), - }, - UnitDef { - _id: "sqm", - symbol: "m²", - aliases: &["m2", "sqm", "square_meter", "square_meters"], - category: Category::Area, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "sqkm", - symbol: "km²", - aliases: &["km2", "sqkm", "square_kilometer"], - category: Category::Area, - conversion: Conversion::Factor(1000000.0), - }, - UnitDef { - _id: "sqft", - symbol: "ft²", - aliases: &["ft2", "sqft", "square_foot", "square_feet"], - category: Category::Area, - conversion: Conversion::Factor(0.092903), - }, - UnitDef { - _id: "acre", - symbol: "ac", - aliases: &["ac", "acre", "acres"], - category: Category::Area, - conversion: Conversion::Factor(4046.86), - }, - UnitDef { - _id: "hectare", - symbol: "ha", - aliases: &["ha", "hectare", "hectares"], - category: Category::Area, - conversion: Conversion::Factor(10000.0), - }, - // Data (base: byte) - UnitDef { - _id: "byte", - symbol: "B", - aliases: &["b", "byte", "bytes"], - category: Category::Data, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "kilobyte", - symbol: "KB", - aliases: &["kb", "kilobyte", "kilobytes"], - category: Category::Data, - conversion: Conversion::Factor(1000.0), - }, - UnitDef { - _id: "megabyte", - symbol: "MB", - aliases: &["mb", "megabyte", "megabytes"], - category: Category::Data, - conversion: Conversion::Factor(1_000_000.0), - }, - UnitDef { - _id: "gigabyte", - symbol: "GB", - aliases: &["gb", "gigabyte", "gigabytes"], - category: Category::Data, - conversion: Conversion::Factor(1_000_000_000.0), - }, - UnitDef { - _id: "terabyte", - symbol: "TB", - aliases: &["tb", "terabyte", "terabytes"], - category: Category::Data, - conversion: Conversion::Factor(1_000_000_000_000.0), - }, - UnitDef { - _id: "kibibyte", - symbol: "KiB", - aliases: &["kib", "kibibyte", "kibibytes"], - category: Category::Data, - conversion: Conversion::Factor(1024.0), - }, - UnitDef { - _id: "mebibyte", - symbol: "MiB", - aliases: &["mib", "mebibyte", "mebibytes"], - category: Category::Data, - conversion: Conversion::Factor(1_048_576.0), - }, - UnitDef { - _id: "gibibyte", - symbol: "GiB", - aliases: &["gib", "gibibyte", "gibibytes"], - category: Category::Data, - conversion: Conversion::Factor(1_073_741_824.0), - }, - UnitDef { - _id: "tebibyte", - symbol: "TiB", - aliases: &["tib", "tebibyte", "tebibytes"], - category: Category::Data, - conversion: Conversion::Factor(1_099_511_627_776.0), - }, - // Time (base: second) - UnitDef { - _id: "second", - symbol: "s", - aliases: &["s", "sec", "second", "seconds"], - category: Category::Time, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "minute", - symbol: "min", - aliases: &["min", "minute", "minutes"], - category: Category::Time, - conversion: Conversion::Factor(60.0), - }, - UnitDef { - _id: "hour", - symbol: "h", - aliases: &["h", "hr", "hour", "hours"], - category: Category::Time, - conversion: Conversion::Factor(3600.0), - }, - UnitDef { - _id: "day", - symbol: "d", - aliases: &["d", "day", "days"], - category: Category::Time, - conversion: Conversion::Factor(86400.0), - }, - UnitDef { - _id: "week", - symbol: "wk", - aliases: &["wk", "week", "weeks"], - category: Category::Time, - conversion: Conversion::Factor(604800.0), - }, - UnitDef { - _id: "month", - symbol: "mo", - aliases: &["mo", "month", "months"], - category: Category::Time, - conversion: Conversion::Factor(2_592_000.0), - }, - UnitDef { - _id: "year", - symbol: "yr", - aliases: &["yr", "year", "years"], - category: Category::Time, - conversion: Conversion::Factor(31_536_000.0), - }, - // Pressure (base: Pa) - UnitDef { - _id: "pascal", - symbol: "Pa", - aliases: &["pa", "pascal", "pascals"], - category: Category::Pressure, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "hectopascal", - symbol: "hPa", - aliases: &["hpa", "hectopascal"], - category: Category::Pressure, - conversion: Conversion::Factor(100.0), - }, - UnitDef { - _id: "kilopascal", - symbol: "kPa", - aliases: &["kpa", "kilopascal"], - category: Category::Pressure, - conversion: Conversion::Factor(1000.0), - }, - UnitDef { - _id: "bar", - symbol: "bar", - aliases: &["bar", "bars"], - category: Category::Pressure, - conversion: Conversion::Factor(100_000.0), - }, - UnitDef { - _id: "millibar", - symbol: "mbar", - aliases: &["mbar", "millibar"], - category: Category::Pressure, - conversion: Conversion::Factor(100.0), - }, - UnitDef { - _id: "psi", - symbol: "psi", - aliases: &["psi", "pounds_per_square_inch"], - category: Category::Pressure, - conversion: Conversion::Factor(6894.76), - }, - UnitDef { - _id: "atmosphere", - symbol: "atm", - aliases: &["atm", "atmosphere", "atmospheres"], - category: Category::Pressure, - conversion: Conversion::Factor(101_325.0), - }, - UnitDef { - _id: "mmhg", - symbol: "mmHg", - aliases: &["mmhg", "torr"], - category: Category::Pressure, - conversion: Conversion::Factor(133.322), - }, - // Energy (base: Joule) - UnitDef { - _id: "joule", - symbol: "J", - aliases: &["j", "joule", "joules"], - category: Category::Energy, - conversion: Conversion::Factor(1.0), - }, - UnitDef { - _id: "kilojoule", - symbol: "kJ", - aliases: &["kj", "kilojoule", "kilojoules"], - category: Category::Energy, - conversion: Conversion::Factor(1000.0), - }, - UnitDef { - _id: "calorie", - symbol: "cal", - aliases: &["cal", "calorie", "calories"], - category: Category::Energy, - conversion: Conversion::Factor(4.184), - }, - UnitDef { - _id: "kilocalorie", - symbol: "kcal", - aliases: &["kcal", "kilocalorie", "kilocalories"], - category: Category::Energy, - conversion: Conversion::Factor(4184.0), - }, - UnitDef { - _id: "watt_hour", - symbol: "Wh", - aliases: &["wh", "watt_hour"], - category: Category::Energy, - conversion: Conversion::Factor(3600.0), - }, - UnitDef { - _id: "kilowatt_hour", - symbol: "kWh", - aliases: &["kwh", "kilowatt_hour"], - category: Category::Energy, - conversion: Conversion::Factor(3_600_000.0), - }, - UnitDef { - _id: "btu", - symbol: "BTU", - aliases: &["btu", "british_thermal_unit"], - category: Category::Energy, - conversion: Conversion::Factor(1055.06), - }, - ] -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_celsius_to_fahrenheit() { - let r = convert_to(&100.0, "c", "f").unwrap(); - assert!((r.value - 212.0).abs() < 0.01); - } - - #[test] - fn test_fahrenheit_to_celsius() { - let r = convert_to(&32.0, "f", "c").unwrap(); - assert!((r.value - 0.0).abs() < 0.01); - } - - #[test] - fn test_body_temp_f_to_c() { - let r = convert_to(&98.6, "f", "c").unwrap(); - assert!((r.value - 37.0).abs() < 0.01); - } - - #[test] - fn test_km_to_miles() { - let r = convert_to(&100.0, "km", "mi").unwrap(); - assert!((r.value - 62.1371).abs() < 0.01); - } - - #[test] - fn test_miles_to_km() { - let r = convert_to(&1.0, "mi", "km").unwrap(); - assert!((r.value - 1.60934).abs() < 0.01); - } - - #[test] - fn test_kg_to_lb() { - let r = convert_to(&1.0, "kg", "lb").unwrap(); - assert!((r.value - 2.20462).abs() < 0.01); - } - - #[test] - fn test_lb_to_kg() { - let r = convert_to(&100.0, "lbs", "kg").unwrap(); - assert!((r.value - 45.3592).abs() < 0.01); - } - - #[test] - fn test_liters_to_gallons() { - let r = convert_to(&3.78541, "l", "gal").unwrap(); - assert!((r.value - 1.0).abs() < 0.01); - } - - #[test] - fn test_kmh_to_mph() { - let r = convert_to(&100.0, "kmh", "mph").unwrap(); - assert!((r.value - 62.1371).abs() < 0.01); - } - - #[test] - fn test_gb_to_mb() { - let r = convert_to(&1.0, "gb", "mb").unwrap(); - assert!((r.value - 1000.0).abs() < 0.01); - } - - #[test] - fn test_gib_to_mib() { - let r = convert_to(&1.0, "gib", "mib").unwrap(); - assert!((r.value - 1024.0).abs() < 0.01); - } - - #[test] - fn test_hours_to_minutes() { - let r = convert_to(&2.5, "h", "min").unwrap(); - assert!((r.value - 150.0).abs() < 0.01); - } - - #[test] - fn test_bar_to_psi() { - let r = convert_to(&1.0, "bar", "psi").unwrap(); - assert!((r.value - 14.5038).abs() < 0.01); - } - - #[test] - fn test_kcal_to_kj() { - let r = convert_to(&1.0, "kcal", "kj").unwrap(); - assert!((r.value - 4.184).abs() < 0.01); - } - - #[test] - fn test_sqm_to_sqft() { - let r = convert_to(&1.0, "m2", "ft2").unwrap(); - assert!((r.value - 10.7639).abs() < 0.01); - } - - #[test] - fn test_unknown_unit_returns_none() { - assert!(convert_to(&100.0, "xyz", "abc").is_none()); - } - - #[test] - fn test_cross_category_returns_none() { - assert!(convert_to(&100.0, "km", "kg").is_none()); - } - - #[test] - fn test_convert_common_returns_results() { - let results = convert_common(&100.0, "km"); - assert!(!results.is_empty()); - assert!(results.len() <= 5); - } - - #[test] - fn test_convert_common_excludes_source() { - let results = convert_common(&100.0, "km"); - for r in &results { - assert_ne!(r.target_symbol, "km"); - } - } - - #[test] - fn test_alias_case_insensitive() { - let r1 = convert_to(&100.0, "KM", "MI").unwrap(); - let r2 = convert_to(&100.0, "km", "mi").unwrap(); - assert!((r1.value - r2.value).abs() < 0.001); - } - - #[test] - fn test_full_name_alias() { - let r = convert_to(&100.0, "kilometers", "miles").unwrap(); - assert!((r.value - 62.1371).abs() < 0.01); - } - - #[test] - fn test_format_currency_two_decimals() { - let r = convert_to(&1.0, "km", "mi").unwrap(); - // display_value should have reasonable formatting - assert!(!r.display_value.is_empty()); - } - - #[test] - fn test_currency_alias_convert_to() { - // "dollar" and "euro" are aliases, not in the UNITS table - let r = convert_to(&20.0, "dollar", "euro"); - // May return None if ECB rates unavailable (network), but should not panic - // In a network-available environment, this should return Some - if let Some(r) = r { - assert!(r.value > 0.0); - assert_eq!(r.target_symbol, "EUR"); - } - } - - #[test] - fn test_currency_alias_convert_common() { - let results = convert_common(&20.0, "dollar"); - // May be empty if ECB rates unavailable, but should not panic - for r in &results { - assert!(r.value > 0.0); - } - } - - #[test] - fn test_display_value_no_double_unit() { - let r = convert_to(&100.0, "km", "mi").unwrap(); - // display_value should contain the symbol exactly once - let count = r.display_value.matches(&r.target_symbol).count(); - assert_eq!(count, 1, "display_value '{}' should contain '{}' exactly once", r.display_value, r.target_symbol); - } -} diff --git a/crates/owlry-plugin-system/Cargo.toml b/crates/owlry-plugin-system/Cargo.toml deleted file mode 100644 index 0ea277f..0000000 --- a/crates/owlry-plugin-system/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "owlry-plugin-system" -version = "1.0.0" -edition.workspace = true -rust-version.workspace = true -license.workspace = true -repository.workspace = true -description = "System plugin for owlry - power and session management commands" -keywords = ["owlry", "plugin", "system", "power"] -categories = ["os"] - -[lib] -crate-type = ["cdylib"] # Compile as dynamic library (.so) - -[dependencies] -# Plugin API for owlry -owlry-plugin-api = { git = "https://somegit.dev/Owlibou/owlry.git", tag = "plugin-api-v1.0.0" } - -# ABI-stable types (re-exported from owlry-plugin-api, but needed for RString etc) -abi_stable = "0.11" diff --git a/crates/owlry-plugin-system/src/lib.rs b/crates/owlry-plugin-system/src/lib.rs deleted file mode 100644 index 5e5b3e9..0000000 --- a/crates/owlry-plugin-system/src/lib.rs +++ /dev/null @@ -1,257 +0,0 @@ -//! System Plugin for Owlry -//! -//! A static provider that provides system power and session management commands. -//! -//! Commands: -//! - Shutdown - Power off the system -//! - Reboot - Restart the system -//! - Reboot into BIOS - Restart into UEFI/BIOS setup -//! - Suspend - Suspend to RAM -//! - Hibernate - Suspend to disk -//! - Lock Screen - Lock the session -//! - Log Out - End the current session - -use abi_stable::std_types::{ROption, RStr, RString, RVec}; -use owlry_plugin_api::{ - API_VERSION, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, - ProviderPosition, owlry_plugin, -}; - -// Plugin metadata -const PLUGIN_ID: &str = "system"; -const PLUGIN_NAME: &str = "System"; -const PLUGIN_VERSION: &str = env!("CARGO_PKG_VERSION"); -const PLUGIN_DESCRIPTION: &str = "Power and session management commands"; - -// Provider metadata -const PROVIDER_ID: &str = "system"; -const PROVIDER_NAME: &str = "System"; -const PROVIDER_PREFIX: &str = ":sys"; -const PROVIDER_ICON: &str = "system-shutdown"; -const PROVIDER_TYPE_ID: &str = "system"; - -/// System provider state - holds cached items -struct SystemState { - items: Vec, -} - -impl SystemState { - fn new() -> Self { - Self { items: Vec::new() } - } - - fn load_commands(&mut self) { - self.items.clear(); - - // Define system commands - // Format: (id, name, description, icon, command) - let commands: &[(&str, &str, &str, &str, &str)] = &[ - ( - "system:shutdown", - "Shutdown", - "Power off the system", - "system-shutdown", - "systemctl poweroff", - ), - ( - "system:reboot", - "Reboot", - "Restart the system", - "system-reboot", - "systemctl reboot", - ), - ( - "system:reboot-bios", - "Reboot into BIOS", - "Restart into UEFI/BIOS setup", - "system-reboot", - "systemctl reboot --firmware-setup", - ), - ( - "system:suspend", - "Suspend", - "Suspend to RAM", - "system-suspend", - "systemctl suspend", - ), - ( - "system:hibernate", - "Hibernate", - "Suspend to disk", - "system-suspend-hibernate", - "systemctl hibernate", - ), - ( - "system:lock", - "Lock Screen", - "Lock the session", - "system-lock-screen", - "loginctl lock-session", - ), - ( - "system:logout", - "Log Out", - "End the current session", - "system-log-out", - "loginctl terminate-session self", - ), - ]; - - for (id, name, description, icon, command) in commands { - self.items.push( - PluginItem::new(*id, *name, *command) - .with_description(*description) - .with_icon(*icon) - .with_keywords(vec!["power".to_string(), "system".to_string()]), - ); - } - } -} - -// ============================================================================ -// Plugin Interface Implementation -// ============================================================================ - -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 { - 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::Static, - type_id: RString::from(PROVIDER_TYPE_ID), - position: ProviderPosition::Normal, - priority: 0, // Static: use frecency ordering - }] - .into() -} - -extern "C" fn provider_init(_provider_id: RStr<'_>) -> ProviderHandle { - let state = Box::new(SystemState::new()); - ProviderHandle::from_box(state) -} - -extern "C" fn provider_refresh(handle: ProviderHandle) -> RVec { - if handle.ptr.is_null() { - return RVec::new(); - } - - // SAFETY: We created this handle from Box - let state = unsafe { &mut *(handle.ptr as *mut SystemState) }; - - // Load/reload commands - state.load_commands(); - - // Return items - state.items.to_vec().into() -} - -extern "C" fn provider_query(_handle: ProviderHandle, _query: RStr<'_>) -> RVec { - // Static provider - query is handled by the core using cached items - RVec::new() -} - -extern "C" fn provider_drop(handle: ProviderHandle) { - if !handle.ptr.is_null() { - // SAFETY: We created this handle from Box - unsafe { - handle.drop_as::(); - } - } -} - -// Register the plugin vtable -owlry_plugin! { - info: plugin_info, - providers: plugin_providers, - init: provider_init, - refresh: provider_refresh, - query: provider_query, - drop: provider_drop, -} - -// ============================================================================ -// Tests -// ============================================================================ - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_system_state_new() { - let state = SystemState::new(); - assert!(state.items.is_empty()); - } - - #[test] - fn test_system_commands_loaded() { - let mut state = SystemState::new(); - state.load_commands(); - - assert!(state.items.len() >= 6); - - // Check for specific commands - let names: Vec<&str> = state.items.iter().map(|i| i.name.as_str()).collect(); - assert!(names.contains(&"Shutdown")); - assert!(names.contains(&"Reboot")); - assert!(names.contains(&"Suspend")); - assert!(names.contains(&"Lock Screen")); - assert!(names.contains(&"Log Out")); - } - - #[test] - fn test_reboot_bios_command() { - let mut state = SystemState::new(); - state.load_commands(); - - let bios_cmd = state - .items - .iter() - .find(|i| i.name.as_str() == "Reboot into BIOS") - .expect("Reboot into BIOS should exist"); - - assert_eq!( - bios_cmd.command.as_str(), - "systemctl reboot --firmware-setup" - ); - } - - #[test] - fn test_commands_have_icons() { - let mut state = SystemState::new(); - state.load_commands(); - - for item in &state.items { - assert!( - item.icon.is_some(), - "Item '{}' should have an icon", - item.name.as_str() - ); - } - } - - #[test] - fn test_commands_have_descriptions() { - let mut state = SystemState::new(); - state.load_commands(); - - for item in &state.items { - assert!( - item.description.is_some(), - "Item '{}' should have a description", - item.name.as_str() - ); - } - } -}