36 KiB
Built-in Providers Migration Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Move calculator, converter, and system from external .so plugins to native providers compiled into owlry-core. Retire 3 plugin AUR packages (transitional) and 4 meta AUR packages (already deleted). Update READMEs.
Architecture: Add a DynamicProvider trait to owlry-core for built-in providers that produce results per-keystroke (calculator, converter). System uses the existing Provider trait (static list). All 3 are registered in ProviderManager::new_with_config() gated by config toggles. Conflict detection skips native .so plugins when a built-in provider with the same type_id exists.
Tech Stack: Rust 1.90+, owlry-core, meval (math), reqwest (currency HTTP), serde
File Map
| File | Action | Responsibility |
|---|---|---|
crates/owlry-core/Cargo.toml |
Modify | Add/move meval, reqwest to required deps |
crates/owlry-core/src/providers/mod.rs |
Modify | Add DynamicProvider trait, builtin_dynamic field, iterate in search, register providers, conflict detection |
crates/owlry-core/src/providers/calculator.rs |
Create | Calculator provider (port from plugin) |
crates/owlry-core/src/providers/system.rs |
Create | System provider (port from plugin) |
crates/owlry-core/src/providers/converter/mod.rs |
Create | Converter provider entry (port from plugin) |
crates/owlry-core/src/providers/converter/parser.rs |
Create | Query parsing |
crates/owlry-core/src/providers/converter/units.rs |
Create | Unit definitions + conversion |
crates/owlry-core/src/providers/converter/currency.rs |
Create | ECB rate fetching + currency conversion |
crates/owlry-core/src/config/mod.rs |
Modify | Add converter toggle |
crates/owlry-core/src/lib.rs |
Modify | Re-export new provider modules if needed |
README.md |
Modify | Update package tables, remove meta section |
Plugins repo (separate commits):
| File | Action | Responsibility |
|---|---|---|
crates/owlry-plugin-calculator/ |
Remove | Retired — built into core |
crates/owlry-plugin-converter/ |
Remove | Retired — built into core |
crates/owlry-plugin-system/ |
Remove | Retired — built into core |
aur/owlry-plugin-calculator/ |
Modify | Transitional PKGBUILD |
aur/owlry-plugin-converter/ |
Modify | Transitional PKGBUILD |
aur/owlry-plugin-system/ |
Modify | Transitional PKGBUILD |
README.md |
Modify | Remove retired plugins |
Task 1: Add dependencies and DynamicProvider trait
Files:
-
Modify:
crates/owlry-core/Cargo.toml -
Modify:
crates/owlry-core/src/providers/mod.rs -
Step 1: Update Cargo.toml — move meval and reqwest to required deps
In crates/owlry-core/Cargo.toml, add to the [dependencies] section (NOT in optional):
# Built-in provider deps
meval = "0.2"
reqwest = { version = "0.13", default-features = false, features = ["rustls", "blocking"] }
Remove meval and reqwest from the [features] section's lua feature list. The lua feature should become:
[features]
default = []
lua = ["dep:mlua"]
dev-logging = []
Remove the meval = { ... optional = true } and reqwest = { ... optional = true } lines from [dependencies] since we're replacing them with required versions.
- Step 2: Add DynamicProvider trait to providers/mod.rs
In crates/owlry-core/src/providers/mod.rs, add after the Provider trait definition (after line 105):
/// Trait for built-in providers that produce results per-keystroke.
/// Unlike static `Provider`s which cache items via `refresh()`/`items()`,
/// dynamic providers generate results on every query.
pub(crate) trait DynamicProvider: Send + Sync {
fn name(&self) -> &str;
fn provider_type(&self) -> ProviderType;
fn query(&self, query: &str) -> Vec<LaunchItem>;
fn priority(&self) -> u32;
}
- Step 3: Add builtin_dynamic field to ProviderManager
In the ProviderManager struct definition, add after the providers field:
/// Built-in dynamic providers (calculator, converter)
/// These are queried per-keystroke, like native dynamic plugins
builtin_dynamic: Vec<Box<dyn DynamicProvider>>,
- Step 4: Initialize the new field in ProviderManager::new()
In ProviderManager::new(), add builtin_dynamic: Vec::new(), to the struct initialization.
- Step 5: Iterate builtin_dynamic in search_with_frecency
In the search_with_frecency method, inside the if !query.is_empty() block, after the existing for provider in &self.dynamic_providers loop, add:
// Built-in dynamic providers (calculator, converter)
for provider in &self.builtin_dynamic {
if !filter.is_active(provider.provider_type()) {
continue;
}
let dynamic_results = provider.query(query);
let base_score = provider.priority() as i64;
let grouping_bonus: i64 = match provider.provider_type() {
ProviderType::Plugin(ref id)
if matches!(id.as_str(), "calc" | "conv") =>
{
10_000
}
_ => 0,
};
for (idx, item) in dynamic_results.into_iter().enumerate() {
results.push((item, base_score + grouping_bonus - idx as i64));
}
}
- Step 6: Verify it compiles
Run: cargo check -p owlry-core
Expected: Compiles (with warnings about unused fields/imports — that's fine, providers aren't registered yet).
- Step 7: Commit
git add crates/owlry-core/Cargo.toml crates/owlry-core/src/providers/mod.rs
git commit -m "feat(core): add DynamicProvider trait and builtin_dynamic support
Foundation for built-in calculator, converter, and system providers.
DynamicProvider trait for per-keystroke providers. ProviderManager
iterates builtin_dynamic alongside native dynamic plugins in search."
Task 2: Port calculator provider
Files:
- Create:
crates/owlry-core/src/providers/calculator.rs - Modify:
crates/owlry-core/src/providers/mod.rs(addmod calculator;)
The calculator plugin (owlry-plugins/crates/owlry-plugin-calculator/src/lib.rs) evaluates math expressions. It supports = expr and calc expr prefix triggers, plus auto-detection of math-like input. Port it to implement DynamicProvider.
- Step 1: Add module declaration
In crates/owlry-core/src/providers/mod.rs, add with the other module declarations at the top:
pub(crate) mod calculator;
- Step 2: Write the calculator provider with tests
Create crates/owlry-core/src/providers/calculator.rs:
//! Built-in calculator provider.
//!
//! Evaluates math expressions and returns the result.
//! Supports `= expr` and `calc expr` prefix triggers, plus auto-detection.
use super::{DynamicProvider, LaunchItem, ProviderType};
const PROVIDER_TYPE_ID: &str = "calc";
pub struct CalculatorProvider;
impl CalculatorProvider {
pub fn new() -> Self {
Self
}
}
impl DynamicProvider for CalculatorProvider {
fn name(&self) -> &str {
"Calculator"
}
fn provider_type(&self) -> ProviderType {
ProviderType::Plugin(PROVIDER_TYPE_ID.into())
}
fn query(&self, query: &str) -> Vec<LaunchItem> {
let input = query.trim();
// Strip trigger prefixes
let expr = if let Some(rest) = input.strip_prefix('=') {
rest.trim()
} else if let Some(rest) = input.strip_prefix("calc ") {
rest.trim()
} else if let Some(rest) = input.strip_prefix("calc\t") {
rest.trim()
} else if looks_like_math(input) {
input
} else {
return Vec::new();
};
if expr.is_empty() {
return Vec::new();
}
match meval::eval_str(expr) {
Ok(result) if result.is_finite() => {
let display = format_result(result);
let description = format!("= {}", expr);
let copy_cmd = format!("printf '%s' '{}' | wl-copy", display);
vec![LaunchItem {
id: format!("calc:{}", expr),
name: display,
description: Some(description),
icon: Some("accessories-calculator".into()),
provider: ProviderType::Plugin(PROVIDER_TYPE_ID.into()),
command: copy_cmd,
terminal: false,
tags: vec!["math".into(), "calculator".into()],
}]
}
_ => Vec::new(),
}
}
fn priority(&self) -> u32 {
10_000
}
}
/// Check if input looks like a math expression (heuristic for auto-detect)
fn looks_like_math(input: &str) -> bool {
if input.is_empty() || input.len() < 2 {
return false;
}
// Must contain at least one operator
let has_operator = input.chars().any(|c| matches!(c, '+' | '-' | '*' | '/' | '^' | '%'));
if !has_operator && !input.contains("sqrt") && !input.contains("sin") && !input.contains("cos") && !input.contains("tan") && !input.contains("log") && !input.contains("ln") && !input.contains("abs") {
return false;
}
// First char should be a digit, minus, open-paren, or function name
let first = input.chars().next().unwrap();
first.is_ascii_digit() || first == '-' || first == '(' || first == '.'
|| input.starts_with("sqrt") || input.starts_with("sin")
|| input.starts_with("cos") || input.starts_with("tan")
|| input.starts_with("log") || input.starts_with("ln")
|| input.starts_with("abs") || input.starts_with("pi")
|| input.starts_with("e ")
}
fn format_result(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!("{:.10}", 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::*;
fn query(input: &str) -> Vec<LaunchItem> {
CalculatorProvider::new().query(input)
}
#[test]
fn test_prefix_equals() {
let r = query("= 5+3");
assert_eq!(r.len(), 1);
assert_eq!(r[0].name, "8");
}
#[test]
fn test_prefix_calc() {
let r = query("calc 10*2");
assert_eq!(r.len(), 1);
assert_eq!(r[0].name, "20");
}
#[test]
fn test_auto_detect() {
let r = query("5+3");
assert_eq!(r.len(), 1);
assert_eq!(r[0].name, "8");
}
#[test]
fn test_complex_expression() {
let r = query("= sqrt(16) + 2^3");
assert_eq!(r.len(), 1);
assert_eq!(r[0].name, "12");
}
#[test]
fn test_decimal_result() {
let r = query("= 10/3");
assert_eq!(r.len(), 1);
assert!(r[0].name.starts_with("3.333"));
}
#[test]
fn test_large_number_separators() {
let r = query("= 1000000");
assert_eq!(r.len(), 1);
assert_eq!(r[0].name, "1,000,000");
}
#[test]
fn test_invalid_expression_returns_empty() {
assert!(query("= hello world").is_empty());
}
#[test]
fn test_plain_text_returns_empty() {
assert!(query("firefox").is_empty());
}
#[test]
fn test_provider_type() {
let p = CalculatorProvider::new();
assert_eq!(p.provider_type(), ProviderType::Plugin("calc".into()));
}
#[test]
fn test_description_shows_expression() {
let r = query("= 2+2");
assert_eq!(r[0].description.as_deref(), Some("= 2+2"));
}
#[test]
fn test_copy_command() {
let r = query("= 42");
assert!(r[0].command.contains("42"));
assert!(r[0].command.contains("wl-copy"));
}
}
- Step 3: Run tests
Run: cargo test -p owlry-core calculator
Expected: All tests pass.
- Step 4: Commit
git add crates/owlry-core/src/providers/calculator.rs crates/owlry-core/src/providers/mod.rs
git commit -m "feat(core): add built-in calculator provider
Port from owlry-plugin-calculator. Implements DynamicProvider trait.
Supports = prefix, calc prefix, and auto-detection of math expressions.
Uses meval for expression evaluation."
Task 3: Port system provider
Files:
- Create:
crates/owlry-core/src/providers/system.rs - Modify:
crates/owlry-core/src/providers/mod.rs(addmod system;)
System is a static provider — it returns a fixed list of actions (shutdown, reboot, etc.) via refresh()/items(). Implements the existing Provider trait.
- Step 1: Add module declaration
In crates/owlry-core/src/providers/mod.rs:
pub(crate) mod system;
- Step 2: Write the system provider with tests
Create crates/owlry-core/src/providers/system.rs:
//! Built-in system provider.
//!
//! Provides power and session management actions: shutdown, reboot, suspend,
//! hibernate, lock screen, and log out.
use super::{LaunchItem, Provider, ProviderType};
const PROVIDER_TYPE_ID: &str = "sys";
const PROVIDER_ICON: &str = "system-shutdown";
struct SystemAction {
id: &'static str,
name: &'static str,
description: &'static str,
icon: &'static str,
command: &'static str,
}
const ACTIONS: &[SystemAction] = &[
SystemAction {
id: "shutdown",
name: "Shutdown",
description: "Power off the system",
icon: "system-shutdown",
command: "systemctl poweroff",
},
SystemAction {
id: "reboot",
name: "Reboot",
description: "Restart the system",
icon: "system-reboot",
command: "systemctl reboot",
},
SystemAction {
id: "reboot-bios",
name: "Reboot to BIOS",
description: "Restart into firmware setup",
icon: "system-reboot",
command: "systemctl reboot --firmware-setup",
},
SystemAction {
id: "suspend",
name: "Suspend",
description: "Suspend the system to RAM",
icon: "system-suspend",
command: "systemctl suspend",
},
SystemAction {
id: "hibernate",
name: "Hibernate",
description: "Hibernate the system to disk",
icon: "system-hibernate",
command: "systemctl hibernate",
},
SystemAction {
id: "lock",
name: "Lock Screen",
description: "Lock the current session",
icon: "system-lock-screen",
command: "loginctl lock-session",
},
SystemAction {
id: "logout",
name: "Log Out",
description: "End the current session",
icon: "system-log-out",
command: "loginctl terminate-session self",
},
];
pub struct SystemProvider {
items: Vec<LaunchItem>,
}
impl SystemProvider {
pub fn new() -> Self {
let items = ACTIONS
.iter()
.map(|a| LaunchItem {
id: format!("sys:{}", a.id),
name: a.name.into(),
description: Some(a.description.into()),
icon: Some(a.icon.into()),
provider: ProviderType::Plugin(PROVIDER_TYPE_ID.into()),
command: a.command.into(),
terminal: false,
tags: vec!["system".into()],
})
.collect();
Self { items }
}
}
impl Provider for SystemProvider {
fn name(&self) -> &str {
"System"
}
fn provider_type(&self) -> ProviderType {
ProviderType::Plugin(PROVIDER_TYPE_ID.into())
}
fn refresh(&mut self) {
// Static list — nothing to refresh
}
fn items(&self) -> &[LaunchItem] {
&self.items
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_has_all_actions() {
let p = SystemProvider::new();
assert_eq!(p.items().len(), 7);
}
#[test]
fn test_action_names() {
let p = SystemProvider::new();
let names: Vec<&str> = p.items().iter().map(|i| i.name.as_str()).collect();
assert!(names.contains(&"Shutdown"));
assert!(names.contains(&"Reboot"));
assert!(names.contains(&"Lock Screen"));
assert!(names.contains(&"Log Out"));
}
#[test]
fn test_provider_type() {
let p = SystemProvider::new();
assert_eq!(p.provider_type(), ProviderType::Plugin("sys".into()));
}
#[test]
fn test_shutdown_command() {
let p = SystemProvider::new();
let shutdown = p.items().iter().find(|i| i.id == "sys:shutdown").unwrap();
assert_eq!(shutdown.command, "systemctl poweroff");
}
#[test]
fn test_all_items_have_system_tag() {
let p = SystemProvider::new();
for item in p.items() {
assert!(item.tags.contains(&"system".into()));
}
}
}
- Step 3: Run tests
Run: cargo test -p owlry-core system
Expected: All tests pass.
- Step 4: Commit
git add crates/owlry-core/src/providers/system.rs crates/owlry-core/src/providers/mod.rs
git commit -m "feat(core): add built-in system provider
Port from owlry-plugin-system. Static provider with 7 actions:
shutdown, reboot, reboot-to-BIOS, suspend, hibernate, lock, logout."
Task 4: Port converter provider
Files:
- Create:
crates/owlry-core/src/providers/converter/mod.rs - Create:
crates/owlry-core/src/providers/converter/parser.rs - Create:
crates/owlry-core/src/providers/converter/units.rs - Create:
crates/owlry-core/src/providers/converter/currency.rs - Modify:
crates/owlry-core/src/providers/mod.rs(addmod converter;)
This is the largest port — 4 files, ~1700 lines. The logic is identical to the plugin; the port removes FFI types (RString, RVec, PluginItem) and uses LaunchItem directly.
- Step 1: Add module declaration
In crates/owlry-core/src/providers/mod.rs:
pub(crate) mod converter;
- Step 2: Create converter/parser.rs
Create crates/owlry-core/src/providers/converter/parser.rs. This is a direct copy from owlry-plugins/crates/owlry-plugin-converter/src/parser.rs with crate:: import paths changed to super:::
Copy the complete file from /home/cnachtigall/ssd/git/archive/owlibou/owlry-plugins/crates/owlry-plugin-converter/src/parser.rs, changing:
use crate::units;→use super::units;
No other changes — the parser has no FFI types.
- Step 3: Create converter/units.rs
Copy the complete file from /home/cnachtigall/ssd/git/archive/owlibou/owlry-plugins/crates/owlry-plugin-converter/src/units.rs, changing:
use crate::currency;→use super::currency;crate::format_with_separators→super::format_with_separators
No other changes — units.rs has no FFI types.
- Step 4: Create converter/currency.rs
Copy the complete file from /home/cnachtigall/ssd/git/archive/owlibou/owlry-plugins/crates/owlry-plugin-converter/src/currency.rs.
No import changes needed — currency.rs uses only std, serde, reqwest, and dirs.
- Step 5: Create converter/mod.rs
Create crates/owlry-core/src/providers/converter/mod.rs. This replaces the plugin's lib.rs, removing all FFI types and implementing DynamicProvider:
//! Built-in converter provider.
//!
//! Converts between units and currencies.
//! Supports `> expr` prefix or auto-detection.
pub(crate) mod currency;
pub(crate) mod parser;
pub(crate) mod units;
use super::{DynamicProvider, LaunchItem, ProviderType};
const PROVIDER_TYPE_ID: &str = "conv";
const PROVIDER_ICON: &str = "edit-find-replace-symbolic";
pub struct ConverterProvider;
impl ConverterProvider {
pub fn new() -> Self {
Self
}
}
impl DynamicProvider for ConverterProvider {
fn name(&self) -> &str {
"Converter"
}
fn provider_type(&self) -> ProviderType {
ProviderType::Plugin(PROVIDER_TYPE_ID.into())
}
fn query(&self, query: &str) -> Vec<LaunchItem> {
let query_str = query.trim();
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 Vec::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| {
LaunchItem {
id: format!("conv:{}:{}:{}", parsed.from_unit, r.target_symbol, r.value),
name: r.display_value.clone(),
description: Some(format!(
"{} {} = {}",
format_number(parsed.value),
parsed.from_symbol,
r.display_value,
)),
icon: Some(PROVIDER_ICON.into()),
provider: ProviderType::Plugin(PROVIDER_TYPE_ID.into()),
command: format!("printf '%s' '{}' | wl-copy", r.raw_value.replace('\'', "'\\''")),
terminal: false,
tags: vec![],
}
})
.collect()
}
fn priority(&self) -> u32 {
9_000
}
}
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()
}
}
pub(crate) 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::*;
fn query(input: &str) -> Vec<LaunchItem> {
ConverterProvider::new().query(input)
}
#[test]
fn test_prefix_trigger() {
let r = query("> 100 km to mi");
assert!(!r.is_empty());
}
#[test]
fn test_auto_detect() {
let r = query("100 km to mi");
assert!(!r.is_empty());
}
#[test]
fn test_no_target_returns_common() {
let r = query("> 100 km");
assert!(r.len() > 1);
}
#[test]
fn test_temperature() {
let r = query("102F to C");
assert!(!r.is_empty());
}
#[test]
fn test_nonsense_returns_empty() {
assert!(query("hello world").is_empty());
}
#[test]
fn test_provider_type() {
let p = ConverterProvider::new();
assert_eq!(p.provider_type(), ProviderType::Plugin("conv".into()));
}
#[test]
fn test_description_no_double_unit() {
let r = query("100 km to mi");
if let Some(item) = r.first() {
let desc = item.description.as_deref().unwrap();
// Should be "100 km = 62.1371 mi", not "100 km = 62.1371 mi mi"
assert!(!desc.ends_with(" mi mi"), "double unit in: {}", desc);
}
}
#[test]
fn test_copy_command() {
let r = query("= 100 km to mi");
if let Some(item) = r.first() {
assert!(item.command.contains("wl-copy"));
}
}
}
- Step 6: Run tests
Run: cargo test -p owlry-core converter
Expected: All converter tests pass (currency tests may skip if network unavailable).
- Step 7: Run all tests
Run: cargo test -p owlry-core --lib
Expected: All tests pass.
- Step 8: Commit
git add crates/owlry-core/src/providers/converter/
git add crates/owlry-core/src/providers/mod.rs
git commit -m "feat(core): add built-in converter provider
Port from owlry-plugin-converter. Implements DynamicProvider trait.
Supports unit conversion (10 categories, 75+ units) and currency
conversion via ECB API with 24-hour file cache."
Task 5: Register providers and add config toggle
Files:
-
Modify:
crates/owlry-core/src/config/mod.rs -
Modify:
crates/owlry-core/src/providers/mod.rs -
Step 1: Add converter config toggle
In crates/owlry-core/src/config/mod.rs, add to the ProvidersConfig struct after the calculator field:
/// Enable converter provider (> expression or auto-detect)
#[serde(default = "default_true")]
pub converter: bool,
Add converter: true, to the Default impl for ProvidersConfig.
- Step 2: Register built-in providers in new_with_config
In crates/owlry-core/src/providers/mod.rs, in new_with_config(), after creating the core_providers vec and before the native plugin loader section, add:
// Built-in dynamic providers
let mut builtin_dynamic: Vec<Box<dyn DynamicProvider>> = Vec::new();
if config.providers.calculator {
builtin_dynamic.push(Box::new(calculator::CalculatorProvider::new()));
info!("Registered built-in calculator provider");
}
if config.providers.converter {
builtin_dynamic.push(Box::new(converter::ConverterProvider::new()));
info!("Registered built-in converter provider");
}
// Built-in static providers
if config.providers.system {
core_providers.push(Box::new(system::SystemProvider::new()));
info!("Registered built-in system provider");
}
Then pass builtin_dynamic into the ProviderManager construction. In ProviderManager::new(), accept it as a parameter:
Change pub fn new(core_providers: Vec<Box<dyn Provider>>, native_providers: Vec<NativeProvider>) -> Self to:
pub fn new(
core_providers: Vec<Box<dyn Provider>>,
native_providers: Vec<NativeProvider>,
builtin_dynamic: Vec<Box<dyn DynamicProvider>>,
) -> Self
And set the field: builtin_dynamic, in the struct initialization.
Update all call sites of ProviderManager::new():
-
In
new_with_config(): pass thebuiltin_dynamicvec -
In
app.rs(owlry crate, dmenu mode): passVec::new()as the third argument -
In
app.rs(local fallback): passVec::new()as the third argument -
Step 3: Verify it compiles and tests pass
Run: cargo check --workspace && cargo test -p owlry-core --lib
Expected: Clean compile, all tests pass.
- Step 4: Commit
git add crates/owlry-core/src/config/mod.rs crates/owlry-core/src/providers/mod.rs crates/owlry/src/app.rs
git commit -m "feat(core): register built-in providers in ProviderManager
Calculator and converter registered as built-in dynamic providers.
System registered as built-in static provider. All gated by config
toggles (calculator, converter, system — default true)."
Task 6: Add conflict detection
Files:
- Modify:
crates/owlry-core/src/providers/mod.rs
When users upgrade to the new owlry-core but still have the old .so plugins installed, both the built-in and native plugin would produce duplicate results. Skip native plugins whose type_id matches a built-in.
- Step 1: Write test
Add to the tests in crates/owlry-core/src/providers/mod.rs:
#[test]
fn test_builtin_type_ids() {
let pm = ProviderManager::new(vec![], vec![], vec![
Box::new(calculator::CalculatorProvider::new()),
Box::new(converter::ConverterProvider::new()),
]);
let ids = pm.builtin_type_ids();
assert!(ids.contains("calc"));
assert!(ids.contains("conv"));
}
- Step 2: Implement builtin_type_ids and conflict detection
Add a helper method to ProviderManager:
/// Get type IDs of built-in dynamic providers (for conflict detection)
fn builtin_type_ids(&self) -> std::collections::HashSet<String> {
let mut ids: std::collections::HashSet<String> = self
.builtin_dynamic
.iter()
.filter_map(|p| match p.provider_type() {
ProviderType::Plugin(id) => Some(id),
_ => None,
})
.collect();
// Also include built-in static providers (system)
for p in &self.providers {
if let ProviderType::Plugin(id) = p.provider_type() {
ids.insert(id);
}
}
ids
}
In new_with_config(), after creating the ProviderManager with Self::new(...), add conflict detection before the native plugin classification loop. In the loop that classifies native providers (around the for provider in native_providers block), add a skip check:
let builtin_ids = manager.builtin_type_ids();
Then inside the loop, before the if provider.is_dynamic() check:
// Skip native plugins that conflict with built-in providers
if builtin_ids.contains(&type_id) {
info!(
"Skipping native plugin '{}' — built-in provider exists",
type_id
);
continue;
}
- Step 3: Run tests
Run: cargo test -p owlry-core --lib
Expected: All tests pass.
- Step 4: Commit
git add crates/owlry-core/src/providers/mod.rs
git commit -m "feat(core): skip native plugins that conflict with built-in providers
When users upgrade owlry-core but still have old .so plugins installed,
the conflict detection skips the native plugin to prevent duplicate
results."
Task 7: Remove meta package AUR dirs from main repo
Files:
-
Remove:
aur/owlry-meta-essentials/ -
Remove:
aur/owlry-meta-full/ -
Remove:
aur/owlry-meta-tools/ -
Remove:
aur/owlry-meta-widgets/ -
Step 1: Remove meta package directories
The meta packages have already been deleted from AUR. Remove the local directories:
# Hide .git dirs, remove from git tracking, restore .git dirs
for pkg in owlry-meta-essentials owlry-meta-full owlry-meta-tools owlry-meta-widgets; do
dir="aur/$pkg"
if [ -d "$dir/.git" ]; then
mv "$dir/.git" "$dir/.git.bak"
git rm -r "$dir"
# Don't restore .git — the dir is gone from tracking
# But keep the local .git.bak in case we need the AUR repo history
fi
done
Actually, since the AUR packages are already deleted, just remove the directories entirely:
for pkg in owlry-meta-essentials owlry-meta-full owlry-meta-tools owlry-meta-widgets; do
rm -rf "aur/$pkg"
done
git add -A aur/
- Step 2: Commit
git commit -m "chore: remove retired meta package AUR dirs
owlry-meta-essentials, owlry-meta-full, owlry-meta-tools, and
owlry-meta-widgets have been deleted from AUR. Remove local dirs."
Task 8: Update main repo README
Files:
-
Modify:
README.md -
Step 1: Update package tables and install instructions
In the README:
- In the AUR install section, remove the meta package lines and update:
# Core (includes calculator, converter, system actions)
yay -S owlry
# Add individual plugins
yay -S owlry-plugin-bookmarks owlry-plugin-weather
- In the "Core packages" table, update owlry-core description:
| `owlry-core` | Headless daemon with built-in calculator, converter, and system providers |
-
Remove the "Meta bundles" table entirely.
-
Remove calculator, converter, and system from the "Plugin packages" table.
-
Update the plugin count in the Features section from "14 native plugins" to "11 plugin packages" or similar.
- Step 2: Commit
git add README.md
git commit -m "docs: update README for built-in providers migration
Calculator, converter, and system are now built into owlry-core.
Remove meta package references. Update install instructions."
Task 9: Remove retired plugins from plugins repo
Files (in owlry-plugins repo):
-
Remove:
crates/owlry-plugin-calculator/ -
Remove:
crates/owlry-plugin-converter/ -
Remove:
crates/owlry-plugin-system/ -
Modify:
Cargo.toml(workspace members) -
Modify:
Cargo.lock -
Step 1: Remove crate directories and update workspace
rm -rf crates/owlry-plugin-calculator crates/owlry-plugin-converter crates/owlry-plugin-system
Edit the workspace Cargo.toml to remove the three members from the [workspace] members list.
Run: cargo check --workspace to verify remaining plugins still build.
- Step 2: Commit
git add -A
git commit -m "chore: remove calculator, converter, system plugins
These providers are now built into owlry-core. The plugins are
retired — transitional AUR packages redirect to owlry-core."
- Step 3: Push
git push
Task 10: Update plugins repo README
Files (in owlry-plugins repo):
-
Modify:
README.md -
Step 1: Update plugin listing
Remove calculator, converter, and system from the plugin listing. Add a note:
> **Note:** Calculator, converter, and system actions are built into `owlry-core` as of v1.2.0.
> They no longer need to be installed separately.
Update the plugin count.
- Step 2: Commit and push
git add README.md
git commit -m "docs: update README — calculator, converter, system moved to core"
git push
Task 11: Transitional AUR packages for retired plugins
Files (in owlry-plugins repo):
-
Modify:
aur/owlry-plugin-calculator/PKGBUILD -
Modify:
aur/owlry-plugin-converter/PKGBUILD -
Modify:
aur/owlry-plugin-system/PKGBUILD -
Step 1: Create transitional PKGBUILDs
For each of the 3 retired plugins, replace the PKGBUILD with a transitional package. Example for calculator (repeat for converter and system):
# Maintainer: vikingowl <christian@nachtigall.dev>
pkgname=owlry-plugin-calculator
pkgver=1.0.1
pkgrel=99
pkgdesc="Transitional package — calculator is now built into owlry-core"
arch=('any')
url="https://somegit.dev/Owlibou/owlry"
license=('GPL-3.0-or-later')
depends=('owlry-core')
replaces=('owlry-plugin-calculator')
No source, prepare(), build(), check(), or package() functions. The pkgrel=99 ensures this version is higher than any previous release.
Regenerate .SRCINFO for each:
cd aur/owlry-plugin-calculator && makepkg --printsrcinfo > .SRCINFO
cd ../owlry-plugin-converter && makepkg --printsrcinfo > .SRCINFO
cd ../owlry-plugin-system && makepkg --printsrcinfo > .SRCINFO
- Step 2: Commit transitional PKGBUILDs to plugins repo
# Stage using the .git workaround
for pkg in owlry-plugin-calculator owlry-plugin-converter owlry-plugin-system; do
just aur-stage "$pkg"
done
git commit -m "chore(aur): transitional packages for retired plugins"
git push
- Step 3: Publish to AUR
for pkg in owlry-plugin-calculator owlry-plugin-converter owlry-plugin-system; do
just aur-publish-pkg "$pkg"
done
Task 12: Tag and deploy owlry-core to AUR
- Step 1: Bump, tag, and deploy
just release-crate owlry-core <new_version>
This runs the full pipeline: bump → push → tag → AUR update → publish.
Execution Notes
Task dependency order
Tasks 1-6 are sequential (each builds on the previous).
Task 7 is independent (can be done anytime).
Task 8 depends on Tasks 1-6 (README reflects built-in providers).
Tasks 9-11 are in the plugins repo and depend on Task 12 (need the new owlry-core version for transitional package depends).
Task 12 depends on Tasks 1-6.
Recommended order: 1 → 2 → 3 → 4 → 5 → 6 → 7 → 8 → 12 → 9 → 10 → 11