feat(converter): scaffold plugin crate with vtable

This commit is contained in:
2026-03-26 15:15:33 +01:00
parent 67a4791828
commit c44502d0ab
6 changed files with 223 additions and 0 deletions

View File

@@ -2,6 +2,7 @@
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",

View File

@@ -0,0 +1,21 @@
[package]
name = "owlry-plugin-converter"
version = "1.0.0"
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"

View File

@@ -0,0 +1,17 @@
use std::collections::HashMap;
pub struct CurrencyRates {
pub rates: HashMap<String, f64>,
}
pub fn get_rates() -> Option<CurrencyRates> {
None
}
pub fn resolve_currency_code(_alias: &str) -> Option<String> {
None
}
pub fn is_currency_alias(_alias: &str) -> bool {
false
}

View File

@@ -0,0 +1,156 @@
//! 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";
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!("sh -c 'echo -n \"{}\" | wl-copy'", r.raw_value),
)
.with_description(format!(
"{} {} = {} {}",
format_number(parsed.value),
parsed.from_symbol,
r.display_value,
r.target_symbol,
))
.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()
}

View File

@@ -0,0 +1,10 @@
pub struct ParsedQuery {
pub value: f64,
pub from_unit: String,
pub from_symbol: String,
pub target_unit: Option<String>,
}
pub fn parse_conversion(_input: &str) -> Option<ParsedQuery> {
None
}

View File

@@ -0,0 +1,18 @@
pub struct ConversionResult {
pub value: f64,
pub raw_value: String,
pub display_value: String,
pub target_symbol: String,
}
pub fn find_unit(_alias: &str) -> Option<&'static str> {
None
}
pub fn convert_to(_value: &f64, _from: &str, _to: &str) -> Option<ConversionResult> {
None
}
pub fn convert_common(_value: &f64, _from: &str) -> Vec<ConversionResult> {
vec![]
}