chore: format, fix clippy warnings, bump all crates to 1.0.0

This commit is contained in:
2026-03-26 13:37:55 +01:00
parent 50caa1ff0d
commit f5d83f1372
53 changed files with 1233 additions and 745 deletions

10
Cargo.lock generated
View File

@@ -2417,7 +2417,7 @@ dependencies = [
[[package]]
name = "owlry"
version = "0.4.10"
version = "1.0.0"
dependencies = [
"chrono",
"clap",
@@ -2437,7 +2437,7 @@ dependencies = [
[[package]]
name = "owlry-core"
version = "0.5.0"
version = "1.0.0"
dependencies = [
"chrono",
"ctrlc",
@@ -2462,7 +2462,7 @@ dependencies = [
[[package]]
name = "owlry-lua"
version = "0.4.10"
version = "1.0.0"
dependencies = [
"abi_stable",
"chrono",
@@ -2480,7 +2480,7 @@ dependencies = [
[[package]]
name = "owlry-plugin-api"
version = "0.4.10"
version = "1.0.0"
dependencies = [
"abi_stable",
"serde",
@@ -2488,7 +2488,7 @@ dependencies = [
[[package]]
name = "owlry-rune"
version = "0.4.10"
version = "1.0.0"
dependencies = [
"chrono",
"dirs",

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-core"
version = "0.5.0"
version = "1.0.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

View File

@@ -73,11 +73,7 @@ fn default_max_results() -> usize {
}
fn default_tabs() -> Vec<String> {
vec![
"app".to_string(),
"cmd".to_string(),
"uuctl".to_string(),
]
vec!["app".to_string(), "cmd".to_string(), "uuctl".to_string()]
}
/// User-customizable theme colors
@@ -143,10 +139,18 @@ impl Default for AppearanceConfig {
}
}
fn default_width() -> i32 { 850 }
fn default_height() -> i32 { 650 }
fn default_font_size() -> u32 { 14 }
fn default_border_radius() -> u32 { 12 }
fn default_width() -> i32 {
850
}
fn default_height() -> i32 {
650
}
fn default_font_size() -> u32 {
14
}
fn default_border_radius() -> u32 {
12
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidersConfig {
@@ -196,7 +200,6 @@ pub struct ProvidersConfig {
pub files: bool,
// ─── Widget Providers ───────────────────────────────────────────────
/// Enable MPRIS media player widget
#[serde(default = "default_true")]
pub media: bool,
@@ -350,28 +353,19 @@ impl PluginsConfig {
/// Get a string value from a plugin's config
#[allow(dead_code)]
pub fn get_plugin_string(&self, plugin_name: &str, key: &str) -> Option<&str> {
self.plugin_configs
.get(plugin_name)?
.get(key)?
.as_str()
self.plugin_configs.get(plugin_name)?.get(key)?.as_str()
}
/// Get an integer value from a plugin's config
#[allow(dead_code)]
pub fn get_plugin_int(&self, plugin_name: &str, key: &str) -> Option<i64> {
self.plugin_configs
.get(plugin_name)?
.get(key)?
.as_integer()
self.plugin_configs.get(plugin_name)?.get(key)?.as_integer()
}
/// Get a boolean value from a plugin's config
#[allow(dead_code)]
pub fn get_plugin_bool(&self, plugin_name: &str, key: &str) -> Option<bool> {
self.plugin_configs
.get(plugin_name)?
.get(key)?
.as_bool()
self.plugin_configs.get(plugin_name)?.get(key)?.as_bool()
}
}
@@ -414,7 +408,6 @@ fn default_pomodoro_break() -> u32 {
5
}
/// Detect the best available terminal emulator
/// Fallback chain:
/// 1. $TERMINAL env var (user's explicit preference)
@@ -427,10 +420,12 @@ fn default_pomodoro_break() -> u32 {
fn detect_terminal() -> String {
// 1. Check $TERMINAL env var first (user's explicit preference)
if let Ok(term) = std::env::var("TERMINAL")
&& !term.is_empty() && command_exists(&term) {
debug!("Using $TERMINAL: {}", term);
return term;
}
&& !term.is_empty()
&& command_exists(&term)
{
debug!("Using $TERMINAL: {}", term);
return term;
}
// 2. Try xdg-terminal-exec (freedesktop standard)
if command_exists("xdg-terminal-exec") {
@@ -454,7 +449,14 @@ fn detect_terminal() -> String {
}
// 5. Common X11/legacy terminals
let legacy_terminals = ["gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal", "tilix", "terminator"];
let legacy_terminals = [
"gnome-terminal",
"konsole",
"xfce4-terminal",
"mate-terminal",
"tilix",
"terminator",
];
for term in legacy_terminals {
if command_exists(term) {
debug!("Found legacy terminal: {}", term);

View File

@@ -94,7 +94,10 @@ impl ProviderFilter {
};
#[cfg(feature = "dev-logging")]
debug!("[Filter] Created with enabled providers: {:?}", filter.enabled);
debug!(
"[Filter] Created with enabled providers: {:?}",
filter.enabled
);
filter
}
@@ -118,13 +121,19 @@ impl ProviderFilter {
self.enabled.insert(ProviderType::Application);
}
#[cfg(feature = "dev-logging")]
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
debug!(
"[Filter] Toggled OFF {:?}, enabled: {:?}",
provider, self.enabled
);
} else {
#[cfg(feature = "dev-logging")]
let provider_debug = format!("{:?}", provider);
self.enabled.insert(provider);
#[cfg(feature = "dev-logging")]
debug!("[Filter] Toggled ON {}, enabled: {:?}", provider_debug, self.enabled);
debug!(
"[Filter] Toggled ON {}, enabled: {:?}",
provider_debug, self.enabled
);
}
}
@@ -151,7 +160,10 @@ impl ProviderFilter {
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
#[cfg(feature = "dev-logging")]
if self.active_prefix != prefix {
debug!("[Filter] Prefix changed: {:?} -> {:?}", self.active_prefix, prefix);
debug!(
"[Filter] Prefix changed: {:?} -> {:?}",
self.active_prefix, prefix
);
}
self.active_prefix = prefix;
}
@@ -190,7 +202,10 @@ impl ProviderFilter {
let tag = rest[..space_idx].to_lowercase();
let query_part = rest[space_idx + 1..].to_string();
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> tag={:?}, query={:?}", query, tag, query_part);
debug!(
"[Filter] parse_query({:?}) -> tag={:?}, query={:?}",
query, tag, query_part
);
return ParsedQuery {
prefix: None,
tag_filter: Some(tag),
@@ -245,7 +260,10 @@ impl ProviderFilter {
for (prefix_str, provider) in core_prefixes {
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
debug!(
"[Filter] parse_query({:?}) -> prefix={:?}, query={:?}",
query, provider, rest
);
return ParsedQuery {
prefix: Some(provider.clone()),
tag_filter: None,
@@ -259,7 +277,10 @@ impl ProviderFilter {
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
let provider = ProviderType::Plugin(type_id.to_string());
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
debug!(
"[Filter] parse_query({:?}) -> prefix={:?}, query={:?}",
query, provider, rest
);
return ParsedQuery {
prefix: Some(provider),
tag_filter: None,
@@ -304,7 +325,10 @@ impl ProviderFilter {
for (prefix_str, provider) in partial_core {
if trimmed == *prefix_str {
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
debug!(
"[Filter] parse_query({:?}) -> partial prefix {:?}",
query, provider
);
return ParsedQuery {
prefix: Some(provider.clone()),
tag_filter: None,
@@ -317,7 +341,10 @@ impl ProviderFilter {
if trimmed == *prefix_str {
let provider = ProviderType::Plugin(type_id.to_string());
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
debug!(
"[Filter] parse_query({:?}) -> partial prefix {:?}",
query, provider
);
return ParsedQuery {
prefix: Some(provider),
tag_filter: None,
@@ -333,7 +360,10 @@ impl ProviderFilter {
};
#[cfg(feature = "dev-logging")]
debug!("[Filter] parse_query({:?}) -> prefix={:?}, tag={:?}, query={:?}", query, result.prefix, result.tag_filter, result.query);
debug!(
"[Filter] parse_query({:?}) -> prefix={:?}, tag={:?}, query={:?}",
query, result.prefix, result.tag_filter, result.query
);
result
}
@@ -396,7 +426,8 @@ impl ProviderFilter {
/// "app"/"apps"/"application" -> Application, "cmd"/"command" -> Command,
/// "dmenu" -> Dmenu, and everything else -> Plugin(id).
pub fn mode_string_to_provider_type(mode: &str) -> ProviderType {
mode.parse::<ProviderType>().unwrap_or_else(|_| ProviderType::Plugin(mode.to_string()))
mode.parse::<ProviderType>()
.unwrap_or_else(|_| ProviderType::Plugin(mode.to_string()))
}
/// Get display name for current mode
@@ -452,7 +483,10 @@ mod tests {
#[test]
fn test_parse_query_plugin_prefix() {
let result = ProviderFilter::parse_query(":calc 5+3");
assert_eq!(result.prefix, Some(ProviderType::Plugin("calc".to_string())));
assert_eq!(
result.prefix,
Some(ProviderType::Plugin("calc".to_string()))
);
assert_eq!(result.query, "5+3");
}
@@ -544,10 +578,7 @@ mod tests {
#[test]
fn test_explicit_mode_filter_rejects_unknown_plugins() {
let filter = ProviderFilter::from_mode_strings(&[
"app".to_string(),
"cmd".to_string(),
]);
let filter = ProviderFilter::from_mode_strings(&["app".to_string(), "cmd".to_string()]);
assert!(filter.is_active(ProviderType::Application));
assert!(filter.is_active(ProviderType::Command));
// Plugins not in the explicit list must be rejected

View File

@@ -29,19 +29,11 @@ pub enum Request {
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Response {
Results {
items: Vec<ResultItem>,
},
Providers {
list: Vec<ProviderDesc>,
},
SubmenuItems {
items: Vec<ResultItem>,
},
Results { items: Vec<ResultItem> },
Providers { list: Vec<ProviderDesc> },
SubmenuItems { items: Vec<ResultItem> },
Ack,
Error {
message: String,
},
Error { message: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]

View File

@@ -32,7 +32,6 @@ pub fn cache_home() -> Option<PathBuf> {
dirs::cache_dir()
}
// =============================================================================
// Owlry-specific directories
// =============================================================================
@@ -175,9 +174,10 @@ pub fn socket_path() -> PathBuf {
/// Ensure parent directory of a file exists
pub fn ensure_parent_dir(path: &std::path::Path) -> std::io::Result<()> {
if let Some(parent) = path.parent()
&& !parent.exists() {
std::fs::create_dir_all(parent)?;
}
&& !parent.exists()
{
std::fs::create_dir_all(parent)?;
}
Ok(())
}

View File

@@ -54,9 +54,9 @@ pub fn register_action_api(lua: &Lua, owlry: &Table, plugin_id: &str) -> LuaResu
.get("name")
.map_err(|_| mlua::Error::external("action.register: 'name' is required"))?;
let _handler: Function = config
.get("handler")
.map_err(|_| mlua::Error::external("action.register: 'handler' function is required"))?;
let _handler: Function = config.get("handler").map_err(|_| {
mlua::Error::external("action.register: 'handler' function is required")
})?;
// Extract optional fields
let icon: Option<String> = config.get("icon").ok();
@@ -166,7 +166,7 @@ pub fn get_actions_for_item(lua: &Lua, item: &Table) -> LuaResult<Vec<ActionRegi
// Check filter if present
if let Ok(filter) = entry.get::<Function>("filter") {
match filter.call::<bool>(item.clone()) {
Ok(true) => {} // Include this action
Ok(true) => {} // Include this action
Ok(false) => continue, // Skip this action
Err(e) => {
log::warn!("Action filter failed: {}", e);
@@ -220,7 +220,8 @@ mod tests {
fn test_action_registration() {
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
return owlry.action.register({
id = "copy-name",
name = "Copy Name",
@@ -229,7 +230,8 @@ mod tests {
-- copy logic here
end
})
"#);
"#,
);
let action_id: String = chunk.call(()).unwrap();
assert_eq!(action_id, "test-plugin:copy-name");
@@ -243,7 +245,8 @@ mod tests {
fn test_action_with_filter() {
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.action.register({
id = "bookmark-action",
name = "Open in Browser",
@@ -252,7 +255,8 @@ mod tests {
end,
handler = function(item) end
})
"#);
"#,
);
chunk.call::<()>(()).unwrap();
// Create bookmark item
@@ -276,14 +280,16 @@ mod tests {
fn test_action_unregister() {
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.action.register({
id = "temp-action",
name = "Temporary",
handler = function(item) end
})
return owlry.action.unregister("temp-action")
"#);
"#,
);
let unregistered: bool = chunk.call(()).unwrap();
assert!(unregistered);
@@ -296,7 +302,8 @@ mod tests {
let lua = setup_lua("test-plugin");
// Register action that sets a global
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
result = nil
owlry.action.register({
id = "test-exec",
@@ -305,7 +312,8 @@ mod tests {
result = item.name
end
})
"#);
"#,
);
chunk.call::<()>(()).unwrap();
// Create test item

View File

@@ -35,9 +35,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
cache_table.set(
"get",
lua.create_function(|lua, key: String| {
let cache = CACHE.lock().map_err(|e| {
mlua::Error::external(format!("Failed to lock cache: {}", e))
})?;
let cache = CACHE
.lock()
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
if let Some(entry) = cache.get(&key) {
if entry.is_expired() {
@@ -50,8 +50,10 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
}
// Parse JSON back to Lua value
let json_value: serde_json::Value = serde_json::from_str(&entry.value)
.map_err(|e| mlua::Error::external(format!("Failed to parse cached value: {}", e)))?;
let json_value: serde_json::Value =
serde_json::from_str(&entry.value).map_err(|e| {
mlua::Error::external(format!("Failed to parse cached value: {}", e))
})?;
json_to_lua(lua, &json_value)
} else {
@@ -75,9 +77,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
expires_at,
};
let mut cache = CACHE.lock().map_err(|e| {
mlua::Error::external(format!("Failed to lock cache: {}", e))
})?;
let mut cache = CACHE
.lock()
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
cache.insert(key, entry);
Ok(true)
@@ -88,9 +90,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
cache_table.set(
"delete",
lua.create_function(|_lua, key: String| {
let mut cache = CACHE.lock().map_err(|e| {
mlua::Error::external(format!("Failed to lock cache: {}", e))
})?;
let mut cache = CACHE
.lock()
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
Ok(cache.remove(&key).is_some())
})?,
@@ -100,9 +102,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
cache_table.set(
"clear",
lua.create_function(|_lua, ()| {
let mut cache = CACHE.lock().map_err(|e| {
mlua::Error::external(format!("Failed to lock cache: {}", e))
})?;
let mut cache = CACHE
.lock()
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
let count = cache.len();
cache.clear();
@@ -114,9 +116,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
cache_table.set(
"has",
lua.create_function(|_lua, key: String| {
let cache = CACHE.lock().map_err(|e| {
mlua::Error::external(format!("Failed to lock cache: {}", e))
})?;
let cache = CACHE
.lock()
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
if let Some(entry) = cache.get(&key) {
Ok(!entry.is_expired())
@@ -249,10 +251,12 @@ mod tests {
let _: bool = chunk.call(()).unwrap();
// Get and verify
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
local t = owlry.cache.get("table_key")
return t.name, t.value
"#);
"#,
);
let (name, value): (String, i32) = chunk.call(()).unwrap();
assert_eq!(name, "test");
assert_eq!(value, 42);
@@ -262,12 +266,14 @@ mod tests {
fn test_cache_delete() {
let lua = setup_lua();
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.cache.set("delete_key", "value")
local existed = owlry.cache.delete("delete_key")
local value = owlry.cache.get("delete_key")
return existed, value
"#);
"#,
);
let (existed, value): (bool, Option<String>) = chunk.call(()).unwrap();
assert!(existed);
assert!(value.is_none());
@@ -277,12 +283,14 @@ mod tests {
fn test_cache_has() {
let lua = setup_lua();
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
local before = owlry.cache.has("has_key")
owlry.cache.set("has_key", "value")
local after = owlry.cache.has("has_key")
return before, after
"#);
"#,
);
let (before, after): (bool, bool) = chunk.call(()).unwrap();
assert!(!before);
assert!(after);

View File

@@ -329,13 +329,15 @@ mod tests {
clear_all_hooks();
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
local called = false
owlry.hook.on("init", function()
called = true
end)
return true
"#);
"#,
);
let result: bool = chunk.call(()).unwrap();
assert!(result);
@@ -349,11 +351,13 @@ mod tests {
clear_all_hooks();
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.hook.on("query", function(q) return q .. "1" end, 10)
owlry.hook.on("query", function(q) return q .. "2" end, 20)
return true
"#);
"#,
);
chunk.call::<()>(()).unwrap();
// Call hooks - higher priority (20) should run first
@@ -367,11 +371,13 @@ mod tests {
clear_all_hooks();
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.hook.on("select", function() end)
owlry.hook.off("select")
return true
"#);
"#,
);
chunk.call::<()>(()).unwrap();
let plugins = get_registered_plugins(HookEvent::Select);
@@ -383,14 +389,16 @@ mod tests {
clear_all_hooks();
let lua = setup_lua("test-plugin");
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.hook.on("pre_launch", function(item)
if item.name == "blocked" then
return false -- cancel launch
end
return true
end)
"#);
"#,
);
chunk.call::<()>(()).unwrap();
// Create a test item table

View File

@@ -26,18 +26,21 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.map_err(|e| mlua::Error::external(format!("Failed to create HTTP client: {}", e)))?;
.map_err(|e| {
mlua::Error::external(format!("Failed to create HTTP client: {}", e))
})?;
let mut request = client.get(&url);
// Add custom headers if provided
if let Some(ref opts) = opts
&& let Ok(headers) = opts.get::<Table>("headers") {
for pair in headers.pairs::<String, String>() {
let (key, value) = pair?;
request = request.header(&key, &value);
}
&& let Ok(headers) = opts.get::<Table>("headers")
{
for pair in headers.pairs::<String, String>() {
let (key, value) = pair?;
request = request.header(&key, &value);
}
}
let response = request
.send()
@@ -45,9 +48,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let status = response.status().as_u16();
let headers = extract_headers(&response);
let body = response
.text()
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?;
let body = response.text().map_err(|e| {
mlua::Error::external(format!("Failed to read response body: {}", e))
})?;
let result = lua.create_table()?;
result.set("status", status)?;
@@ -78,18 +81,21 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.map_err(|e| mlua::Error::external(format!("Failed to create HTTP client: {}", e)))?;
.map_err(|e| {
mlua::Error::external(format!("Failed to create HTTP client: {}", e))
})?;
let mut request = client.post(&url);
// Add custom headers if provided
if let Some(ref opts) = opts
&& let Ok(headers) = opts.get::<Table>("headers") {
for pair in headers.pairs::<String, String>() {
let (key, value) = pair?;
request = request.header(&key, &value);
}
&& let Ok(headers) = opts.get::<Table>("headers")
{
for pair in headers.pairs::<String, String>() {
let (key, value) = pair?;
request = request.header(&key, &value);
}
}
// Set body based on type
request = match body {
@@ -102,11 +108,7 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
.body(json_str)
}
Value::Nil => request,
_ => {
return Err(mlua::Error::external(
"POST body must be a string or table",
))
}
_ => return Err(mlua::Error::external("POST body must be a string or table")),
};
let response = request
@@ -115,9 +117,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let status = response.status().as_u16();
let headers = extract_headers(&response);
let body = response
.text()
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?;
let body = response.text().map_err(|e| {
mlua::Error::external(format!("Failed to read response body: {}", e))
})?;
let result = lua.create_table()?;
result.set("status", status)?;
@@ -149,19 +151,22 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(timeout_secs))
.build()
.map_err(|e| mlua::Error::external(format!("Failed to create HTTP client: {}", e)))?;
.map_err(|e| {
mlua::Error::external(format!("Failed to create HTTP client: {}", e))
})?;
let mut request = client.get(&url);
request = request.header("Accept", "application/json");
// Add custom headers if provided
if let Some(ref opts) = opts
&& let Ok(headers) = opts.get::<Table>("headers") {
for pair in headers.pairs::<String, String>() {
let (key, value) = pair?;
request = request.header(&key, &value);
}
&& let Ok(headers) = opts.get::<Table>("headers")
{
for pair in headers.pairs::<String, String>() {
let (key, value) = pair?;
request = request.header(&key, &value);
}
}
let response = request
.send()
@@ -174,9 +179,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
)));
}
let body = response
.text()
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?;
let body = response.text().map_err(|e| {
mlua::Error::external(format!("Failed to read response body: {}", e))
})?;
// Parse JSON and convert to Lua table
let json_value: serde_json::Value = serde_json::from_str(&body)

View File

@@ -14,20 +14,20 @@ pub fn register_math_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
// Returns (result, nil) on success or (nil, error_message) on failure
math_table.set(
"calculate",
lua.create_function(|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> {
match meval::eval_str(&expr) {
Ok(result) => {
if result.is_finite() {
Ok((Some(result), None))
} else {
Ok((None, Some("Result is not a finite number".to_string())))
lua.create_function(
|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> {
match meval::eval_str(&expr) {
Ok(result) => {
if result.is_finite() {
Ok((Some(result), None))
} else {
Ok((None, Some("Result is not a finite number".to_string())))
}
}
Err(e) => Ok((None, Some(e.to_string()))),
}
Err(e) => {
Ok((None, Some(e.to_string())))
}
}
})?,
},
)?,
)?;
// owlry.math.calc(expression) -> number (throws on error)
@@ -106,11 +106,13 @@ mod tests {
fn test_calculate_basic() {
let lua = setup_lua();
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
local result, err = owlry.math.calculate("2 + 2")
if err then error(err) end
return result
"#);
"#,
);
let result: f64 = chunk.call(()).unwrap();
assert!((result - 4.0).abs() < f64::EPSILON);
}
@@ -119,11 +121,13 @@ mod tests {
fn test_calculate_complex() {
let lua = setup_lua();
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
local result, err = owlry.math.calculate("sqrt(16) + 2^3")
if err then error(err) end
return result
"#);
"#,
);
let result: f64 = chunk.call(()).unwrap();
assert!((result - 12.0).abs() < f64::EPSILON); // sqrt(16) = 4, 2^3 = 8
}
@@ -132,14 +136,16 @@ mod tests {
fn test_calculate_error() {
let lua = setup_lua();
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
local result, err = owlry.math.calculate("invalid expression @@")
if result then
return false -- should not succeed
else
return true -- correctly failed
end
"#);
"#,
);
let had_error: bool = chunk.call(()).unwrap();
assert!(had_error);
}

View File

@@ -27,8 +27,14 @@ pub fn register_process_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
.map_err(|e| mlua::Error::external(format!("Failed to run command: {}", e)))?;
let result = lua.create_table()?;
result.set("stdout", String::from_utf8_lossy(&output.stdout).to_string())?;
result.set("stderr", String::from_utf8_lossy(&output.stderr).to_string())?;
result.set(
"stdout",
String::from_utf8_lossy(&output.stdout).to_string(),
)?;
result.set(
"stderr",
String::from_utf8_lossy(&output.stderr).to_string(),
)?;
result.set("exit_code", output.status.code().unwrap_or(-1))?;
result.set("success", output.status.success())?;
@@ -95,9 +101,7 @@ pub fn register_env_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
// owlry.env.get(name) -> string or nil
env_table.set(
"get",
lua.create_function(|_lua, name: String| {
Ok(std::env::var(&name).ok())
})?,
lua.create_function(|_lua, name: String| Ok(std::env::var(&name).ok()))?,
)?;
// owlry.env.get_or(name, default) -> string
@@ -166,7 +170,8 @@ mod tests {
assert!(exists);
// Made-up command should not exist
let chunk = lua.load(r#"return owlry.process.exists("this_command_definitely_does_not_exist_12345")"#);
let chunk = lua
.load(r#"return owlry.process.exists("this_command_definitely_does_not_exist_12345")"#);
let not_exists: bool = chunk.call(()).unwrap();
assert!(!not_exists);
}
@@ -190,7 +195,8 @@ mod tests {
fn test_env_get_or() {
let lua = setup_lua();
let chunk = lua.load(r#"return owlry.env.get_or("THIS_VAR_DOES_NOT_EXIST_12345", "default_value")"#);
let chunk = lua
.load(r#"return owlry.env.get_or("THIS_VAR_DOES_NOT_EXIST_12345", "default_value")"#);
let result: String = chunk.call(()).unwrap();
assert_eq!(result, "default_value");
}

View File

@@ -21,7 +21,12 @@ pub struct ThemeRegistration {
}
/// Register theme APIs
pub fn register_theme_api(lua: &Lua, owlry: &Table, plugin_id: &str, plugin_dir: &Path) -> LuaResult<()> {
pub fn register_theme_api(
lua: &Lua,
owlry: &Table,
plugin_id: &str,
plugin_dir: &Path,
) -> LuaResult<()> {
let theme_table = lua.create_table()?;
let plugin_id_owned = plugin_id.to_string();
let plugin_dir_owned = plugin_dir.to_path_buf();
@@ -50,9 +55,7 @@ pub fn register_theme_api(lua: &Lua, owlry: &Table, plugin_id: &str, plugin_dir:
.get("name")
.map_err(|_| mlua::Error::external("theme.register: 'name' is required"))?;
let display_name: String = config
.get("display_name")
.unwrap_or_else(|_| name.clone());
let display_name: String = config.get("display_name").unwrap_or_else(|_| name.clone());
// Get CSS either directly or from file
let css: String = if let Ok(css_str) = config.get::<String>("css") {
@@ -197,13 +200,15 @@ mod tests {
let temp = TempDir::new().unwrap();
let lua = setup_lua("test-plugin", temp.path());
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
return owlry.theme.register({
name = "my-theme",
display_name = "My Theme",
css = ".owlry-window { background: #333; }"
})
"#);
"#,
);
let name: String = chunk.call(()).unwrap();
assert_eq!(name, "my-theme");
@@ -221,12 +226,14 @@ mod tests {
let lua = setup_lua("test-plugin", temp.path());
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
return owlry.theme.register({
name = "file-theme",
css_file = "theme.css"
})
"#);
"#,
);
let name: String = chunk.call(()).unwrap();
assert_eq!(name, "file-theme");
@@ -240,11 +247,13 @@ mod tests {
let temp = TempDir::new().unwrap();
let lua = setup_lua("test-plugin", temp.path());
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.theme.register({ name = "theme1", css = "a{}" })
owlry.theme.register({ name = "theme2", css = "b{}" })
return owlry.theme.list()
"#);
"#,
);
let list: Table = chunk.call(()).unwrap();
let mut names: Vec<String> = Vec::new();
@@ -262,10 +271,12 @@ mod tests {
let temp = TempDir::new().unwrap();
let lua = setup_lua("test-plugin", temp.path());
let chunk = lua.load(r#"
let chunk = lua.load(
r#"
owlry.theme.register({ name = "temp-theme", css = "c{}" })
return owlry.theme.unregister("temp-theme")
"#);
"#,
);
let unregistered: bool = chunk.call(()).unwrap();
assert!(unregistered);

View File

@@ -189,9 +189,10 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResult
// Ensure parent directory exists
if let Some(parent) = full_path.parent()
&& !parent.exists()
&& let Err(e) = std::fs::create_dir_all(parent) {
return Ok((false, Value::String(lua.create_string(e.to_string())?)));
}
&& let Err(e) = std::fs::create_dir_all(parent)
{
return Ok((false, Value::String(lua.create_string(e.to_string())?)));
}
match std::fs::write(&full_path, content) {
Ok(()) => Ok((true, Value::Nil)),
@@ -295,7 +296,8 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResult
use std::os::unix::fs::PermissionsExt;
let plugin_dir: String = lua.named_registry_value("plugin_dir")?;
let full_path = resolve_plugin_path(&plugin_dir, &path);
let is_exec = full_path.metadata()
let is_exec = full_path
.metadata()
.map(|m| m.permissions().mode() & 0o111 != 0)
.unwrap_or(false);
Ok(is_exec)
@@ -335,28 +337,24 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
// owlry.json.encode(value) -> string or nil, error
json_table.set(
"encode",
lua.create_function(|lua, value: Value| {
match lua_to_json(&value) {
Ok(json) => match serde_json::to_string(&json) {
Ok(s) => Ok((Some(s), Value::Nil)),
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
},
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
}
lua.create_function(|lua, value: Value| match lua_to_json(&value) {
Ok(json) => match serde_json::to_string(&json) {
Ok(s) => Ok((Some(s), Value::Nil)),
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
},
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
})?,
)?;
// owlry.json.encode_pretty(value) -> string or nil, error
json_table.set(
"encode_pretty",
lua.create_function(|lua, value: Value| {
match lua_to_json(&value) {
Ok(json) => match serde_json::to_string_pretty(&json) {
Ok(s) => Ok((Some(s), Value::Nil)),
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
},
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
}
lua.create_function(|lua, value: Value| match lua_to_json(&value) {
Ok(json) => match serde_json::to_string_pretty(&json) {
Ok(s) => Ok((Some(s), Value::Nil)),
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
},
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
})?,
)?;
@@ -388,13 +386,16 @@ fn lua_to_json(value: &Value) -> Result<serde_json::Value, String> {
.map(serde_json::Value::Number)
.ok_or_else(|| "Invalid number".to_string()),
Value::String(s) => Ok(serde_json::Value::String(
s.to_str().map_err(|e| e.to_string())?.to_string()
s.to_str().map_err(|e| e.to_string())?.to_string(),
)),
Value::Table(t) => {
// Check if it's an array (sequential integer keys starting from 1)
let len = t.raw_len();
let is_array = len > 0
&& (1..=len).all(|i| t.raw_get::<Value>(i).is_ok_and(|v| !matches!(v, Value::Nil)));
&& (1..=len).all(|i| {
t.raw_get::<Value>(i)
.is_ok_and(|v| !matches!(v, Value::Nil))
});
if is_array {
let arr: Result<Vec<serde_json::Value>, String> = (1..=len)
@@ -475,9 +476,13 @@ mod tests {
fn test_log_api() {
let (lua, _temp) = create_test_lua();
// Just verify it doesn't panic - using call instead of the e-word
lua.load("owlry.log.info('test message')").call::<()>(()).unwrap();
lua.load("owlry.log.info('test message')")
.call::<()>(())
.unwrap();
lua.load("owlry.log.debug('debug')").call::<()>(()).unwrap();
lua.load("owlry.log.warn('warning')").call::<()>(()).unwrap();
lua.load("owlry.log.warn('warning')")
.call::<()>(())
.unwrap();
lua.load("owlry.log.error('error')").call::<()>(()).unwrap();
}
@@ -485,10 +490,7 @@ mod tests {
fn test_path_api() {
let (lua, _temp) = create_test_lua();
let home: String = lua
.load("return owlry.path.home()")
.call(())
.unwrap();
let home: String = lua.load("return owlry.path.home()").call(()).unwrap();
assert!(!home.is_empty());
let joined: String = lua

View File

@@ -7,7 +7,7 @@ use mlua::Lua;
use super::api;
use super::error::{PluginError, PluginResult};
use super::manifest::PluginManifest;
use super::runtime::{create_lua_runtime, load_file, SandboxConfig};
use super::runtime::{SandboxConfig, create_lua_runtime, load_file};
/// A loaded plugin instance
#[derive(Debug)]
@@ -94,7 +94,10 @@ impl LoadedPlugin {
}
/// Call a provider's refresh function
pub fn call_provider_refresh(&self, provider_name: &str) -> PluginResult<Vec<super::PluginItem>> {
pub fn call_provider_refresh(
&self,
provider_name: &str,
) -> PluginResult<Vec<super::PluginItem>> {
let lua = self.lua.as_ref().ok_or_else(|| PluginError::LuaError {
plugin: self.id().to_string(),
message: "Plugin not initialized".to_string(),
@@ -108,7 +111,11 @@ impl LoadedPlugin {
/// Call a provider's query function
#[allow(dead_code)] // Will be used for dynamic query providers
pub fn call_provider_query(&self, provider_name: &str, query: &str) -> PluginResult<Vec<super::PluginItem>> {
pub fn call_provider_query(
&self,
provider_name: &str,
query: &str,
) -> PluginResult<Vec<super::PluginItem>> {
let lua = self.lua.as_ref().ok_or_else(|| PluginError::LuaError {
plugin: self.id().to_string(),
message: "Plugin not initialized".to_string(),
@@ -138,8 +145,8 @@ impl LoadedPlugin {
#[cfg(test)]
mod tests {
use super::*;
use super::super::manifest::{check_compatibility, discover_plugins};
use super::*;
use std::fs;
use std::path::Path;
use tempfile::TempDir;

View File

@@ -112,11 +112,16 @@ pub struct PluginPermissions {
/// Discover all plugins in a directory
///
/// Returns a map of plugin ID -> (manifest, path)
pub fn discover_plugins(plugins_dir: &Path) -> PluginResult<HashMap<String, (PluginManifest, PathBuf)>> {
pub fn discover_plugins(
plugins_dir: &Path,
) -> PluginResult<HashMap<String, (PluginManifest, PathBuf)>> {
let mut plugins = HashMap::new();
if !plugins_dir.exists() {
log::debug!("Plugins directory does not exist: {}", plugins_dir.display());
log::debug!(
"Plugins directory does not exist: {}",
plugins_dir.display()
);
return Ok(plugins);
}
@@ -143,7 +148,11 @@ pub fn discover_plugins(plugins_dir: &Path) -> PluginResult<HashMap<String, (Plu
log::warn!("Duplicate plugin ID '{}', skipping {}", id, path.display());
continue;
}
log::info!("Discovered plugin: {} v{}", manifest.plugin.name, manifest.plugin.version);
log::info!(
"Discovered plugin: {} v{}",
manifest.plugin.name,
manifest.plugin.version
);
plugins.insert(id, (manifest, path));
}
Err(e) => {
@@ -204,7 +213,12 @@ impl PluginManifest {
});
}
if !self.plugin.id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
if !self
.plugin
.id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err(PluginError::InvalidManifest {
plugin: self.plugin.id.clone(),
message: "Plugin ID must be lowercase alphanumeric with hyphens".to_string(),
@@ -223,7 +237,10 @@ impl PluginManifest {
if semver::VersionReq::parse(&self.plugin.owlry_version).is_err() {
return Err(PluginError::InvalidManifest {
plugin: self.plugin.id.clone(),
message: format!("Invalid owlry_version constraint: {}", self.plugin.owlry_version),
message: format!(
"Invalid owlry_version constraint: {}",
self.plugin.owlry_version
),
});
}

View File

@@ -50,7 +50,7 @@ pub use loader::LoadedPlugin;
// Used by plugins/commands.rs for plugin CLI commands
#[allow(unused_imports)]
pub use manifest::{check_compatibility, discover_plugins, PluginManifest};
pub use manifest::{PluginManifest, check_compatibility, discover_plugins};
// ============================================================================
// Lua Plugin Manager (only available with lua feature)
@@ -64,7 +64,7 @@ mod lua_manager {
use std::path::PathBuf;
use std::rc::Rc;
use manifest::{discover_plugins, check_compatibility};
use manifest::{check_compatibility, discover_plugins};
/// Plugin manager coordinates loading, initialization, and lifecycle of Lua plugins
pub struct PluginManager {
@@ -158,7 +158,10 @@ mod lua_manager {
/// Get all enabled plugins
pub fn enabled_plugins(&self) -> impl Iterator<Item = Rc<RefCell<LoadedPlugin>>> + '_ {
self.plugins.values().filter(|p| p.borrow().enabled).cloned()
self.plugins
.values()
.filter(|p| p.borrow().enabled)
.cloned()
}
/// Get the number of loaded plugins
@@ -176,7 +179,10 @@ mod lua_manager {
/// Enable a plugin by ID
#[allow(dead_code)]
pub fn enable(&mut self, id: &str) -> PluginResult<()> {
let plugin_rc = self.plugins.get(id).ok_or_else(|| PluginError::NotFound(id.to_string()))?;
let plugin_rc = self
.plugins
.get(id)
.ok_or_else(|| PluginError::NotFound(id.to_string()))?;
let mut plugin = plugin_rc.borrow_mut();
if !plugin.enabled {
@@ -191,7 +197,10 @@ mod lua_manager {
/// Disable a plugin by ID
#[allow(dead_code)]
pub fn disable(&mut self, id: &str) -> PluginResult<()> {
let plugin_rc = self.plugins.get(id).ok_or_else(|| PluginError::NotFound(id.to_string()))?;
let plugin_rc = self
.plugins
.get(id)
.ok_or_else(|| PluginError::NotFound(id.to_string()))?;
plugin_rc.borrow_mut().enabled = false;
Ok(())
}
@@ -200,7 +209,13 @@ mod lua_manager {
#[allow(dead_code)]
pub fn providers_for(&self, provider_name: &str) -> Vec<String> {
self.enabled_plugins()
.filter(|p| p.borrow().manifest.provides.providers.contains(&provider_name.to_string()))
.filter(|p| {
p.borrow()
.manifest
.provides
.providers
.contains(&provider_name.to_string())
})
.map(|p| p.borrow().id().to_string())
.collect()
}
@@ -208,13 +223,15 @@ mod lua_manager {
/// Check if any plugin provides actions
#[allow(dead_code)]
pub fn has_action_plugins(&self) -> bool {
self.enabled_plugins().any(|p| p.borrow().manifest.provides.actions)
self.enabled_plugins()
.any(|p| p.borrow().manifest.provides.actions)
}
/// Check if any plugin provides hooks
#[allow(dead_code)]
pub fn has_hook_plugins(&self) -> bool {
self.enabled_plugins().any(|p| p.borrow().manifest.provides.hooks)
self.enabled_plugins()
.any(|p| p.borrow().manifest.provides.hooks)
}
/// Get all theme names provided by plugins

View File

@@ -17,8 +17,8 @@ use std::sync::{Arc, Once};
use libloading::Library;
use log::{debug, error, info, warn};
use owlry_plugin_api::{
HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo, ProviderKind,
RStr, API_VERSION,
API_VERSION, HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo,
ProviderKind, RStr,
};
use crate::notify;
@@ -28,9 +28,18 @@ use crate::notify;
// ============================================================================
/// Host notification handler
extern "C" fn host_notify(summary: RStr<'_>, body: RStr<'_>, icon: RStr<'_>, urgency: NotifyUrgency) {
extern "C" fn host_notify(
summary: RStr<'_>,
body: RStr<'_>,
icon: RStr<'_>,
urgency: NotifyUrgency,
) {
let icon_str = icon.as_str();
let icon_opt = if icon_str.is_empty() { None } else { Some(icon_str) };
let icon_opt = if icon_str.is_empty() {
None
} else {
Some(icon_str)
};
let notify_urgency = match urgency {
NotifyUrgency::Low => notify::NotifyUrgency::Low,
@@ -121,7 +130,9 @@ impl NativePlugin {
handle: ProviderHandle,
query: &str,
) -> Vec<owlry_plugin_api::PluginItem> {
(self.vtable.provider_query)(handle, query.into()).into_iter().collect()
(self.vtable.provider_query)(handle, query.into())
.into_iter()
.collect()
}
/// Drop a provider handle

View File

@@ -110,9 +110,10 @@ impl RegistryClient {
if let Ok(metadata) = fs::metadata(&cache_path)
&& let Ok(modified) = metadata.modified()
&& let Ok(elapsed) = SystemTime::now().duration_since(modified) {
return elapsed < CACHE_DURATION;
}
&& let Ok(elapsed) = SystemTime::now().duration_since(modified)
{
return elapsed < CACHE_DURATION;
}
false
}
@@ -120,11 +121,13 @@ impl RegistryClient {
/// Fetch the registry index (from cache or network)
pub fn fetch_index(&self, force_refresh: bool) -> Result<RegistryIndex, String> {
// Use cache if valid and not forcing refresh
if !force_refresh && self.is_cache_valid()
if !force_refresh
&& self.is_cache_valid()
&& let Ok(content) = fs::read_to_string(self.cache_path())
&& let Ok(index) = toml::from_str(&content) {
return Ok(index);
}
&& let Ok(index) = toml::from_str(&content)
{
return Ok(index);
}
// Fetch from network
self.fetch_from_network()
@@ -134,12 +137,7 @@ impl RegistryClient {
fn fetch_from_network(&self) -> Result<RegistryIndex, String> {
// Use curl for fetching (available on most systems)
let output = std::process::Command::new("curl")
.args([
"-fsSL",
"--max-time",
"30",
&self.registry_url,
])
.args(["-fsSL", "--max-time", "30", &self.registry_url])
.output()
.map_err(|e| format!("Failed to run curl: {}", e))?;
@@ -185,7 +183,9 @@ impl RegistryClient {
p.id.to_lowercase().contains(&query_lower)
|| p.name.to_lowercase().contains(&query_lower)
|| p.description.to_lowercase().contains(&query_lower)
|| p.tags.iter().any(|t| t.to_lowercase().contains(&query_lower))
|| p.tags
.iter()
.any(|t| t.to_lowercase().contains(&query_lower))
})
.collect();
@@ -210,8 +210,7 @@ impl RegistryClient {
pub fn clear_cache(&self) -> Result<(), String> {
let cache_path = self.cache_path();
if cache_path.exists() {
fs::remove_file(&cache_path)
.map_err(|e| format!("Failed to remove cache: {}", e))?;
fs::remove_file(&cache_path).map_err(|e| format!("Failed to remove cache: {}", e))?;
}
Ok(())
}

View File

@@ -26,7 +26,7 @@ impl Default for SandboxConfig {
allow_commands: false,
allow_network: false,
allow_external_fs: false,
max_run_time_ms: 5000, // 5 seconds
max_run_time_ms: 5000, // 5 seconds
max_memory: 64 * 1024 * 1024, // 64 MB
}
}
@@ -49,11 +49,7 @@ pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> {
// Create Lua with safe standard libraries only
// ALL_SAFE excludes: debug, io, os (dangerous parts), package (loadlib), ffi
// We then customize the os table to only allow safe functions
let libs = StdLib::COROUTINE
| StdLib::TABLE
| StdLib::STRING
| StdLib::UTF8
| StdLib::MATH;
let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
let lua = Lua::new_with(libs, mlua::LuaOptions::default())?;
@@ -75,9 +71,15 @@ fn setup_safe_globals(lua: &Lua) -> LuaResult<()> {
// We do NOT include: os.exit, os.remove, os.rename, os.setlocale, os.tmpname
// and the shell-related functions
let os_table = lua.create_table()?;
os_table.set("clock", lua.create_function(|_, ()| Ok(std::time::Instant::now().elapsed().as_secs_f64()))?)?;
os_table.set(
"clock",
lua.create_function(|_, ()| Ok(std::time::Instant::now().elapsed().as_secs_f64()))?,
)?;
os_table.set("date", lua.create_function(os_date)?)?;
os_table.set("difftime", lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?)?;
os_table.set(
"difftime",
lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?,
)?;
os_table.set("time", lua.create_function(os_time)?)?;
globals.set("os", os_table)?;
@@ -107,8 +109,7 @@ fn os_time(_lua: &Lua, _args: ()) -> LuaResult<i64> {
/// Load and run a Lua file in the given runtime
pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> {
let content = std::fs::read_to_string(path)
.map_err(mlua::Error::external)?;
let content = std::fs::read_to_string(path).map_err(mlua::Error::external)?;
lua.load(&content)
.set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk"))
.into_function()?

View File

@@ -59,7 +59,11 @@ pub struct ScriptRuntimeVTable {
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<ScriptProviderInfo>,
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
pub query: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem>,
pub query: extern "C" fn(
handle: RuntimeHandle,
provider_id: RStr<'_>,
query: RStr<'_>,
) -> RVec<PluginItem>,
pub drop: extern "C" fn(handle: RuntimeHandle),
}
@@ -100,9 +104,8 @@ impl LoadedRuntime {
}
// SAFETY: We trust the runtime library to be correct
let library = unsafe { Library::new(library_path) }.map_err(|e| {
PluginError::LoadError(format!("{}: {}", library_path.display(), e))
})?;
let library = unsafe { Library::new(library_path) }
.map_err(|e| PluginError::LoadError(format!("{}: {}", library_path.display(), e)))?;
let library = Arc::new(library);
@@ -152,12 +155,8 @@ impl LoadedRuntime {
self.providers
.iter()
.map(|info| {
let provider = RuntimeProvider::new(
self.name,
self.vtable,
self.handle,
info.clone(),
);
let provider =
RuntimeProvider::new(self.name, self.vtable, self.handle, info.clone());
Box::new(provider) as Box<dyn Provider>
})
.collect()
@@ -227,7 +226,10 @@ impl Provider for RuntimeProvider {
let name_rstr = RStr::from_str(self.info.name.as_str());
let items_rvec = (self.vtable.refresh)(self.handle, name_rstr);
self.items = items_rvec.into_iter().map(|i| self.convert_item(i)).collect();
self.items = items_rvec
.into_iter()
.map(|i| self.convert_item(i))
.collect();
log::debug!(
"[RuntimeProvider] '{}' refreshed with {} items",
@@ -246,12 +248,16 @@ unsafe impl Send for RuntimeProvider {}
/// Check if the Lua runtime is available
pub fn lua_runtime_available() -> bool {
PathBuf::from(SYSTEM_RUNTIMES_DIR).join("liblua.so").exists()
PathBuf::from(SYSTEM_RUNTIMES_DIR)
.join("liblua.so")
.exists()
}
/// Check if the Rune runtime is available
pub fn rune_runtime_available() -> bool {
PathBuf::from(SYSTEM_RUNTIMES_DIR).join("librune.so").exists()
PathBuf::from(SYSTEM_RUNTIMES_DIR)
.join("librune.so")
.exists()
}
impl LoadedRuntime {

View File

@@ -66,13 +66,14 @@ fn clean_desktop_exec_field(cmd: &str) -> String {
cleaned
}
#[derive(Default)]
pub struct ApplicationProvider {
items: Vec<LaunchItem>,
}
impl ApplicationProvider {
pub fn new() -> Self {
Self { items: Vec::new() }
Self::default()
}
fn get_application_dirs() -> Vec<std::path::PathBuf> {
@@ -139,15 +140,18 @@ impl Provider for ApplicationProvider {
if !current_desktops.is_empty() {
// OnlyShowIn: if set, current desktop must be in the list
if desktop_entry.only_show_in().is_some_and(|only| {
!current_desktops.iter().any(|de| only.contains(&de.as_str()))
!current_desktops
.iter()
.any(|de| only.contains(&de.as_str()))
}) {
continue;
}
// NotShowIn: if current desktop is in the list, skip
if desktop_entry.not_show_in().is_some_and(|not| {
current_desktops.iter().any(|de| not.contains(&de.as_str()))
}) {
if desktop_entry
.not_show_in()
.is_some_and(|not| current_desktops.iter().any(|de| not.contains(&de.as_str())))
{
continue;
}
}
@@ -197,7 +201,8 @@ impl Provider for ApplicationProvider {
);
// Sort alphabetically by name
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
self.items
.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
}
fn items(&self) -> &[LaunchItem] {
@@ -219,7 +224,10 @@ mod tests {
#[test]
fn test_clean_desktop_exec_multiple_placeholders() {
assert_eq!(clean_desktop_exec_field("app %f %u %U"), "app");
assert_eq!(clean_desktop_exec_field("app --flag %u --other"), "app --flag --other");
assert_eq!(
clean_desktop_exec_field("app --flag %u --other"),
"app --flag --other"
);
}
#[test]

View File

@@ -4,13 +4,14 @@ use std::collections::HashSet;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
#[derive(Default)]
pub struct CommandProvider {
items: Vec<LaunchItem>,
}
impl CommandProvider {
pub fn new() -> Self {
Self { items: Vec::new() }
Self::default()
}
fn get_path_dirs() -> Vec<PathBuf> {
@@ -97,7 +98,8 @@ impl Provider for CommandProvider {
debug!("Found {} commands in PATH", self.items.len());
// Sort alphabetically
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
self.items
.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
}
fn items(&self) -> &[LaunchItem] {

View File

@@ -95,9 +95,7 @@ impl Provider for LuaProvider {
unsafe impl Send for LuaProvider {}
/// Create LuaProviders from all registered providers in a plugin
pub fn create_providers_from_plugin(
plugin: Rc<RefCell<LoadedPlugin>>,
) -> Vec<Box<dyn Provider>> {
pub fn create_providers_from_plugin(plugin: Rc<RefCell<LoadedPlugin>>) -> Vec<Box<dyn Provider>> {
let registrations = {
let p = plugin.borrow();
match p.get_provider_registrations() {

View File

@@ -141,13 +141,25 @@ impl ProviderManager {
let type_id = provider.type_id();
if provider.is_dynamic() {
info!("Registered dynamic provider: {} ({})", provider.name(), type_id);
info!(
"Registered dynamic provider: {} ({})",
provider.name(),
type_id
);
manager.dynamic_providers.push(provider);
} else if provider.is_widget() {
info!("Registered widget provider: {} ({})", provider.name(), type_id);
info!(
"Registered widget provider: {} ({})",
provider.name(),
type_id
);
manager.widget_providers.push(provider);
} else {
info!("Registered static provider: {} ({})", provider.name(), type_id);
info!(
"Registered static provider: {} ({})",
provider.name(),
type_id
);
manager.static_native_providers.push(provider);
}
}
@@ -263,15 +275,25 @@ impl ProviderManager {
/// Searches in all native provider lists (static, dynamic, widget)
pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> {
// Check static native providers first (clipboard, emoji, ssh, systemd, etc.)
if let Some(p) = self.static_native_providers.iter().find(|p| p.type_id() == type_id) {
if let Some(p) = self
.static_native_providers
.iter()
.find(|p| p.type_id() == type_id)
{
return Some(p);
}
// Check widget providers (pomodoro, weather, media)
if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) {
if let Some(p) = self
.widget_providers
.iter()
.find(|p| p.type_id() == type_id)
{
return Some(p);
}
// Then dynamic providers (calc, websearch, filesearch)
self.dynamic_providers.iter().find(|p| p.type_id() == type_id)
self.dynamic_providers
.iter()
.find(|p| p.type_id() == type_id)
}
/// Execute a plugin action command
@@ -311,27 +333,31 @@ impl ProviderManager {
/// Iterate over all static provider items (core + native static plugins)
fn all_static_items(&self) -> impl Iterator<Item = &LaunchItem> {
self.providers
.iter()
.flat_map(|p| p.items().iter())
.chain(self.static_native_providers.iter().flat_map(|p| p.items().iter()))
self.providers.iter().flat_map(|p| p.items().iter()).chain(
self.static_native_providers
.iter()
.flat_map(|p| p.items().iter()),
)
}
#[allow(dead_code)]
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
if query.is_empty() {
// Return recent/popular items when query is empty
return self.all_static_items()
return self
.all_static_items()
.take(max_results)
.map(|item| (item.clone(), 0))
.collect();
}
let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
let mut results: Vec<(LaunchItem, i64)> = self
.all_static_items()
.filter_map(|item| {
// Match against name and description
let name_score = self.matcher.fuzzy_match(&item.name, query);
let desc_score = item.description
let desc_score = item
.description
.as_ref()
.and_then(|d| self.matcher.fuzzy_match(d, query));
@@ -417,7 +443,10 @@ impl ProviderManager {
tag_filter: Option<&str>,
) -> Vec<(LaunchItem, i64)> {
#[cfg(feature = "dev-logging")]
debug!("[Search] query={:?}, max={}, frecency_weight={}", query, max_results, frecency_weight);
debug!(
"[Search] query={:?}, max={}, frecency_weight={}",
query, max_results, frecency_weight
);
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
@@ -567,7 +596,13 @@ impl ProviderManager {
{
debug!("[Search] Returning {} results", results.len());
for (i, (item, score)) in results.iter().take(5).enumerate() {
debug!("[Search] #{}: {} (score={}, provider={:?})", i + 1, item.name, score, item.provider);
debug!(
"[Search] #{}: {} (score={}, provider={:?})",
i + 1,
item.name,
score,
item.provider
);
}
if results.len() > 5 {
debug!("[Search] ... and {} more", results.len() - 5);
@@ -583,7 +618,11 @@ impl ProviderManager {
self.providers
.iter()
.map(|p| p.provider_type())
.chain(self.static_native_providers.iter().map(|p| p.provider_type()))
.chain(
self.static_native_providers
.iter()
.map(|p| p.provider_type()),
)
.collect()
}
@@ -606,16 +645,10 @@ impl ProviderManager {
Some(":cmd".to_string()),
"utilities-terminal".to_string(),
),
ProviderType::Dmenu => (
"dmenu".to_string(),
None,
"view-list-symbolic".to_string(),
),
ProviderType::Plugin(type_id) => (
type_id,
None,
"application-x-addon".to_string(),
),
ProviderType::Dmenu => {
("dmenu".to_string(), None, "view-list-symbolic".to_string())
}
ProviderType::Plugin(type_id) => (type_id, None, "application-x-addon".to_string()),
};
descs.push(ProviderDescriptor {
id,
@@ -771,7 +804,10 @@ impl ProviderManager {
}
#[cfg(feature = "dev-logging")]
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);
debug!(
"[Submenu] No submenu actions found for plugin '{}'",
plugin_id
);
None
}
@@ -856,9 +892,8 @@ mod tests {
#[test]
fn test_available_providers_dmenu() {
let providers: Vec<Box<dyn Provider>> = vec![
Box::new(MockProvider::new("dmenu", ProviderType::Dmenu)),
];
let providers: Vec<Box<dyn Provider>> =
vec![Box::new(MockProvider::new("dmenu", ProviderType::Dmenu))];
let pm = ProviderManager::new(providers, Vec::new());
let descs = pm.available_providers();
assert_eq!(descs.len(), 1);
@@ -895,9 +930,10 @@ mod tests {
#[test]
fn test_refresh_provider_unknown_does_not_panic() {
let providers: Vec<Box<dyn Provider>> = vec![
Box::new(MockProvider::new("Applications", ProviderType::Application)),
];
let providers: Vec<Box<dyn Provider>> = vec![Box::new(MockProvider::new(
"Applications",
ProviderType::Application,
))];
let mut pm = ProviderManager::new(providers, Vec::new());
pm.refresh_provider("nonexistent");
// Should complete without panicking
@@ -909,8 +945,8 @@ mod tests {
make_item("firefox", "Firefox", ProviderType::Application),
make_item("vim", "Vim", ProviderType::Application),
];
let provider = MockProvider::new("Applications", ProviderType::Application)
.with_items(items);
let provider =
MockProvider::new("Applications", ProviderType::Application).with_items(items);
let providers: Vec<Box<dyn Provider>> = vec![Box::new(provider)];
let pm = ProviderManager::new(providers, Vec::new());

View File

@@ -9,7 +9,9 @@
use std::sync::{Arc, RwLock};
use log::debug;
use owlry_plugin_api::{PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition};
use owlry_plugin_api::{
PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition,
};
use super::{LaunchItem, Provider, ProviderType};
use crate::plugins::native_loader::NativePlugin;
@@ -76,7 +78,10 @@ impl NativeProvider {
}
let api_items = self.plugin.query_provider(self.handle, query);
api_items.into_iter().map(|item| self.convert_item(item)).collect()
api_items
.into_iter()
.map(|item| self.convert_item(item))
.collect()
}
/// Check if this provider has a prefix that matches the query

View File

@@ -141,8 +141,14 @@ impl Server {
let pm_guard = pm.lock().unwrap();
let frecency_guard = frecency.lock().unwrap();
let results =
pm_guard.search_with_frecency(text, max, &filter, &frecency_guard, weight, None);
let results = pm_guard.search_with_frecency(
text,
max,
&filter,
&frecency_guard,
weight,
None,
);
Response::Results {
items: results
@@ -152,7 +158,10 @@ impl Server {
}
}
Request::Launch { item_id, provider: _ } => {
Request::Launch {
item_id,
provider: _,
} => {
let mut frecency_guard = frecency.lock().unwrap();
frecency_guard.record_launch(item_id);
Response::Ack

View File

@@ -122,7 +122,8 @@ fn test_plugin_action_request() {
#[test]
fn test_terminal_field_defaults_false() {
// terminal field should default to false when missing from JSON
let json = r#"{"id":"test","title":"Test","description":"","icon":"","provider":"cmd","score":0}"#;
let json =
r#"{"id":"test","title":"Test","description":"","icon":"","provider":"cmd","score":0}"#;
let item: ResultItem = serde_json::from_str(json).unwrap();
assert!(!item.terminal);
}

View File

@@ -37,7 +37,11 @@ fn test_server_responds_to_providers_request() {
match resp {
Response::Providers { list } => {
// The default ProviderManager always has at least Application and Command
assert!(list.len() >= 2, "expected at least 2 providers, got {}", list.len());
assert!(
list.len() >= 2,
"expected at least 2 providers, got {}",
list.len()
);
let ids: Vec<&str> = list.iter().map(|p| p.id.as_str()).collect();
assert!(ids.contains(&"app"), "missing 'app' provider");
assert!(ids.contains(&"cmd"), "missing 'cmd' provider");
@@ -95,7 +99,10 @@ fn test_server_handles_query_request() {
Response::Results { items } => {
// A nonsense query should return empty or very few results
// (no items will fuzzy-match "nonexistent_query_xyz")
assert!(items.len() <= 5, "expected few/no results for gibberish query");
assert!(
items.len() <= 5,
"expected few/no results for gibberish query"
);
}
other => panic!("expected Results response, got: {:?}", other),
}
@@ -172,7 +179,10 @@ fn test_server_handles_submenu_for_unknown_plugin() {
"error should mention the plugin id"
);
}
other => panic!("expected Error response for unknown plugin, got: {:?}", other),
other => panic!(
"expected Error response for unknown plugin, got: {:?}",
other
),
}
drop(stream);

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-lua"
version = "0.4.10"
version = "1.0.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

View File

@@ -24,11 +24,14 @@ pub fn register_provider_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
/// Implementation of owlry.provider.register()
fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> {
let name: String = config.get("name")?;
let display_name: String = config.get::<Option<String>>("display_name")?
let display_name: String = config
.get::<Option<String>>("display_name")?
.unwrap_or_else(|| name.clone());
let type_id: String = config.get::<Option<String>>("type_id")?
let type_id: String = config
.get::<Option<String>>("type_id")?
.unwrap_or_else(|| name.replace('-', "_"));
let default_icon: String = config.get::<Option<String>>("default_icon")?
let default_icon: String = config
.get::<Option<String>>("default_icon")?
.unwrap_or_else(|| "application-x-addon".to_string());
let prefix: Option<String> = config.get("prefix")?;
@@ -116,13 +119,14 @@ fn call_provider_function(
// First check if there's a _providers table
if let Ok(Value::Table(providers)) = globals.get::<Value>("_owlry_providers")
&& let Ok(Value::Table(config)) = providers.get::<Value>(provider_name)
&& let Ok(Value::Function(func)) = config.get::<Value>(function_name) {
let result: Value = match query {
Some(q) => func.call(q)?,
None => func.call(())?,
};
return parse_items_result(result);
}
&& let Ok(Value::Function(func)) = config.get::<Value>(function_name)
{
let result: Value = match query {
Some(q) => func.call(q)?,
None => func.call(())?,
};
return parse_items_result(result);
}
// Fall back: search through globals for functions
// This is less reliable but handles simple cases
@@ -153,7 +157,9 @@ fn parse_item(table: &Table) -> LuaResult<PluginItem> {
let description: Option<String> = table.get("description")?;
let icon: Option<String> = table.get("icon")?;
let terminal: bool = table.get::<Option<bool>>("terminal")?.unwrap_or(false);
let tags: Vec<String> = table.get::<Option<Vec<String>>>("tags")?.unwrap_or_default();
let tags: Vec<String> = table
.get::<Option<Vec<String>>>("tags")?
.unwrap_or_default();
let mut item = PluginItem::new(id, name, command);
@@ -176,7 +182,7 @@ fn parse_item(table: &Table) -> LuaResult<PluginItem> {
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::{create_lua_runtime, SandboxConfig};
use crate::runtime::{SandboxConfig, create_lua_runtime};
#[test]
fn test_register_static_provider() {

View File

@@ -11,25 +11,37 @@ use std::path::{Path, PathBuf};
pub fn register_log_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let log = lua.create_table()?;
log.set("debug", lua.create_function(|_, msg: String| {
eprintln!("[DEBUG] {}", msg);
Ok(())
})?)?;
log.set(
"debug",
lua.create_function(|_, msg: String| {
eprintln!("[DEBUG] {}", msg);
Ok(())
})?,
)?;
log.set("info", lua.create_function(|_, msg: String| {
eprintln!("[INFO] {}", msg);
Ok(())
})?)?;
log.set(
"info",
lua.create_function(|_, msg: String| {
eprintln!("[INFO] {}", msg);
Ok(())
})?,
)?;
log.set("warn", lua.create_function(|_, msg: String| {
eprintln!("[WARN] {}", msg);
Ok(())
})?)?;
log.set(
"warn",
lua.create_function(|_, msg: String| {
eprintln!("[WARN] {}", msg);
Ok(())
})?,
)?;
log.set("error", lua.create_function(|_, msg: String| {
eprintln!("[ERROR] {}", msg);
Ok(())
})?)?;
log.set(
"error",
lua.create_function(|_, msg: String| {
eprintln!("[ERROR] {}", msg);
Ok(())
})?,
)?;
owlry.set("log", log)?;
Ok(())
@@ -44,59 +56,79 @@ pub fn register_path_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResu
let path = lua.create_table()?;
// owlry.path.config() -> ~/.config/owlry
path.set("config", lua.create_function(|_, ()| {
Ok(dirs::config_dir()
.map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?)?;
path.set(
"config",
lua.create_function(|_, ()| {
Ok(dirs::config_dir()
.map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?,
)?;
// owlry.path.data() -> ~/.local/share/owlry
path.set("data", lua.create_function(|_, ()| {
Ok(dirs::data_dir()
.map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?)?;
path.set(
"data",
lua.create_function(|_, ()| {
Ok(dirs::data_dir()
.map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?,
)?;
// owlry.path.cache() -> ~/.cache/owlry
path.set("cache", lua.create_function(|_, ()| {
Ok(dirs::cache_dir()
.map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?)?;
path.set(
"cache",
lua.create_function(|_, ()| {
Ok(dirs::cache_dir()
.map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?,
)?;
// owlry.path.home() -> ~
path.set("home", lua.create_function(|_, ()| {
Ok(dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?)?;
path.set(
"home",
lua.create_function(|_, ()| {
Ok(dirs::home_dir()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_default())
})?,
)?;
// owlry.path.join(...) -> joined path
path.set("join", lua.create_function(|_, parts: mlua::Variadic<String>| {
let mut path = PathBuf::new();
for part in parts {
path.push(part);
}
Ok(path.to_string_lossy().to_string())
})?)?;
path.set(
"join",
lua.create_function(|_, parts: mlua::Variadic<String>| {
let mut path = PathBuf::new();
for part in parts {
path.push(part);
}
Ok(path.to_string_lossy().to_string())
})?,
)?;
// owlry.path.plugin_dir() -> plugin directory
let plugin_dir_str = plugin_dir.to_string_lossy().to_string();
path.set("plugin_dir", lua.create_function(move |_, ()| {
Ok(plugin_dir_str.clone())
})?)?;
path.set(
"plugin_dir",
lua.create_function(move |_, ()| Ok(plugin_dir_str.clone()))?,
)?;
// owlry.path.expand(path) -> expanded path (~ -> home)
path.set("expand", lua.create_function(|_, path: String| {
if path.starts_with("~/")
&& let Some(home) = dirs::home_dir() {
path.set(
"expand",
lua.create_function(|_, path: String| {
if path.starts_with("~/")
&& let Some(home) = dirs::home_dir()
{
return Ok(home.join(&path[2..]).to_string_lossy().to_string());
}
Ok(path)
})?)?;
Ok(path)
})?,
)?;
owlry.set("path", path)?;
Ok(())
@@ -111,76 +143,95 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, _plugin_dir: &Path) -> LuaResul
let fs = lua.create_table()?;
// owlry.fs.exists(path) -> bool
fs.set("exists", lua.create_function(|_, path: String| {
let path = expand_path(&path);
Ok(Path::new(&path).exists())
})?)?;
fs.set(
"exists",
lua.create_function(|_, path: String| {
let path = expand_path(&path);
Ok(Path::new(&path).exists())
})?,
)?;
// owlry.fs.is_dir(path) -> bool
fs.set("is_dir", lua.create_function(|_, path: String| {
let path = expand_path(&path);
Ok(Path::new(&path).is_dir())
})?)?;
fs.set(
"is_dir",
lua.create_function(|_, path: String| {
let path = expand_path(&path);
Ok(Path::new(&path).is_dir())
})?,
)?;
// owlry.fs.read(path) -> string or nil
fs.set("read", lua.create_function(|_, path: String| {
let path = expand_path(&path);
match std::fs::read_to_string(&path) {
Ok(content) => Ok(Some(content)),
Err(_) => Ok(None),
}
})?)?;
fs.set(
"read",
lua.create_function(|_, path: String| {
let path = expand_path(&path);
match std::fs::read_to_string(&path) {
Ok(content) => Ok(Some(content)),
Err(_) => Ok(None),
}
})?,
)?;
// owlry.fs.read_lines(path) -> table of strings or nil
fs.set("read_lines", lua.create_function(|lua, path: String| {
let path = expand_path(&path);
match std::fs::read_to_string(&path) {
Ok(content) => {
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
Ok(Some(lua.create_sequence_from(lines)?))
fs.set(
"read_lines",
lua.create_function(|lua, path: String| {
let path = expand_path(&path);
match std::fs::read_to_string(&path) {
Ok(content) => {
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
Ok(Some(lua.create_sequence_from(lines)?))
}
Err(_) => Ok(None),
}
Err(_) => Ok(None),
}
})?)?;
})?,
)?;
// owlry.fs.list_dir(path) -> table of filenames or nil
fs.set("list_dir", lua.create_function(|lua, path: String| {
let path = expand_path(&path);
match std::fs::read_dir(&path) {
Ok(entries) => {
let names: Vec<String> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.collect();
Ok(Some(lua.create_sequence_from(names)?))
fs.set(
"list_dir",
lua.create_function(|lua, path: String| {
let path = expand_path(&path);
match std::fs::read_dir(&path) {
Ok(entries) => {
let names: Vec<String> = entries
.filter_map(|e| e.ok())
.filter_map(|e| e.file_name().into_string().ok())
.collect();
Ok(Some(lua.create_sequence_from(names)?))
}
Err(_) => Ok(None),
}
Err(_) => Ok(None),
}
})?)?;
})?,
)?;
// owlry.fs.read_json(path) -> table or nil
fs.set("read_json", lua.create_function(|lua, path: String| {
let path = expand_path(&path);
match std::fs::read_to_string(&path) {
Ok(content) => {
match serde_json::from_str::<serde_json::Value>(&content) {
fs.set(
"read_json",
lua.create_function(|lua, path: String| {
let path = expand_path(&path);
match std::fs::read_to_string(&path) {
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
Ok(value) => json_to_lua(lua, &value),
Err(_) => Ok(Value::Nil),
}
},
Err(_) => Ok(Value::Nil),
}
Err(_) => Ok(Value::Nil),
}
})?)?;
})?,
)?;
// owlry.fs.write(path, content) -> bool
fs.set("write", lua.create_function(|_, (path, content): (String, String)| {
let path = expand_path(&path);
// Create parent directories if needed
if let Some(parent) = Path::new(&path).parent() {
let _ = std::fs::create_dir_all(parent);
}
Ok(std::fs::write(&path, content).is_ok())
})?)?;
fs.set(
"write",
lua.create_function(|_, (path, content): (String, String)| {
let path = expand_path(&path);
// Create parent directories if needed
if let Some(parent) = Path::new(&path).parent() {
let _ = std::fs::create_dir_all(parent);
}
Ok(std::fs::write(&path, content).is_ok())
})?,
)?;
owlry.set("fs", fs)?;
Ok(())
@@ -195,18 +246,24 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let json = lua.create_table()?;
// owlry.json.encode(value) -> string
json.set("encode", lua.create_function(|lua, value: Value| {
let json_value = lua_to_json(lua, &value)?;
Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string()))
})?)?;
json.set(
"encode",
lua.create_function(|lua, value: Value| {
let json_value = lua_to_json(lua, &value)?;
Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string()))
})?,
)?;
// owlry.json.decode(string) -> value or nil
json.set("decode", lua.create_function(|lua, s: String| {
match serde_json::from_str::<serde_json::Value>(&s) {
Ok(value) => json_to_lua(lua, &value),
Err(_) => Ok(Value::Nil),
}
})?)?;
json.set(
"decode",
lua.create_function(|lua, s: String| {
match serde_json::from_str::<serde_json::Value>(&s) {
Ok(value) => json_to_lua(lua, &value),
Err(_) => Ok(Value::Nil),
}
})?,
)?;
owlry.set("json", json)?;
Ok(())
@@ -219,9 +276,10 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
/// Expand ~ in paths
fn expand_path(path: &str) -> String {
if path.starts_with("~/")
&& let Some(home) = dirs::home_dir() {
return home.join(&path[2..]).to_string_lossy().to_string();
}
&& let Some(home) = dirs::home_dir()
{
return home.join(&path[2..]).to_string_lossy().to_string();
}
path.to_string()
}
@@ -305,7 +363,7 @@ fn lua_to_json(_lua: &Lua, value: &Value) -> LuaResult<serde_json::Value> {
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::{create_lua_runtime, SandboxConfig};
use crate::runtime::{SandboxConfig, create_lua_runtime};
#[test]
fn test_log_api() {
@@ -316,7 +374,10 @@ mod tests {
lua.globals().set("owlry", owlry).unwrap();
// Just verify it doesn't panic
lua.load("owlry.log.info('test message')").set_name("test").call::<()>(()).unwrap();
lua.load("owlry.log.info('test message')")
.set_name("test")
.call::<()>(())
.unwrap();
}
#[test]
@@ -327,10 +388,18 @@ mod tests {
register_path_api(&lua, &owlry, Path::new("/tmp/test-plugin")).unwrap();
lua.globals().set("owlry", owlry).unwrap();
let home: String = lua.load("return owlry.path.home()").set_name("test").call(()).unwrap();
let home: String = lua
.load("return owlry.path.home()")
.set_name("test")
.call(())
.unwrap();
assert!(!home.is_empty());
let plugin_dir: String = lua.load("return owlry.path.plugin_dir()").set_name("test").call(()).unwrap();
let plugin_dir: String = lua
.load("return owlry.path.plugin_dir()")
.set_name("test")
.call(())
.unwrap();
assert_eq!(plugin_dir, "/tmp/test-plugin");
}
@@ -342,10 +411,18 @@ mod tests {
register_fs_api(&lua, &owlry, Path::new("/tmp")).unwrap();
lua.globals().set("owlry", owlry).unwrap();
let exists: bool = lua.load("return owlry.fs.exists('/tmp')").set_name("test").call(()).unwrap();
let exists: bool = lua
.load("return owlry.fs.exists('/tmp')")
.set_name("test")
.call(())
.unwrap();
assert!(exists);
let is_dir: bool = lua.load("return owlry.fs.is_dir('/tmp')").set_name("test").call(()).unwrap();
let is_dir: bool = lua
.load("return owlry.fs.is_dir('/tmp')")
.set_name("test")
.call(())
.unwrap();
assert!(is_dir);
}

View File

@@ -54,7 +54,11 @@ pub struct LuaRuntimeVTable {
/// Refresh a provider's items
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
/// Query a dynamic provider
pub query: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem>,
pub query: extern "C" fn(
handle: RuntimeHandle,
provider_id: RStr<'_>,
query: RStr<'_>,
) -> RVec<PluginItem>,
/// Cleanup and drop the runtime
pub drop: extern "C" fn(handle: RuntimeHandle),
}
@@ -83,11 +87,15 @@ impl RuntimeHandle {
/// Create a null handle (reserved for error cases)
#[allow(dead_code)]
fn null() -> Self {
Self { ptr: std::ptr::null_mut() }
Self {
ptr: std::ptr::null_mut(),
}
}
fn from_box<T>(state: Box<T>) -> Self {
Self { ptr: Box::into_raw(state) as *mut () }
Self {
ptr: Box::into_raw(state) as *mut (),
}
}
unsafe fn drop_as<T>(&self) {
@@ -147,7 +155,10 @@ impl LuaRuntimeState {
for (id, (manifest, path)) in discovered {
// Check version compatibility
if !manifest.is_compatible_with(owlry_version) {
eprintln!("owlry-lua: Plugin '{}' not compatible with owlry {}", id, owlry_version);
eprintln!(
"owlry-lua: Plugin '{}' not compatible with owlry {}",
id, owlry_version
);
continue;
}
@@ -285,13 +296,19 @@ extern "C" fn runtime_refresh(handle: RuntimeHandle, provider_id: RStr<'_>) -> R
state.refresh_provider(provider_id.as_str()).into()
}
extern "C" fn runtime_query(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem> {
extern "C" fn runtime_query(
handle: RuntimeHandle,
provider_id: RStr<'_>,
query: RStr<'_>,
) -> RVec<PluginItem> {
if handle.ptr.is_null() {
return RVec::new();
}
let state = unsafe { &*(handle.ptr as *const LuaRuntimeState) };
state.query_provider(provider_id.as_str(), query.as_str()).into()
state
.query_provider(provider_id.as_str(), query.as_str())
.into()
}
extern "C" fn runtime_drop(handle: RuntimeHandle) {

View File

@@ -8,7 +8,7 @@ use owlry_plugin_api::PluginItem;
use crate::api;
use crate::manifest::PluginManifest;
use crate::runtime::{create_lua_runtime, load_file, SandboxConfig};
use crate::runtime::{SandboxConfig, create_lua_runtime, load_file};
/// Provider registration info from Lua
#[derive(Debug, Clone)]
@@ -77,11 +77,13 @@ impl LoadedPlugin {
// Load the entry point file
let entry_path = self.path.join(&self.manifest.plugin.entry);
if !entry_path.exists() {
return Err(format!("Entry point '{}' not found", self.manifest.plugin.entry));
return Err(format!(
"Entry point '{}' not found",
self.manifest.plugin.entry
));
}
load_file(&lua, &entry_path)
.map_err(|e| format!("Failed to load entry point: {}", e))?;
load_file(&lua, &entry_path).map_err(|e| format!("Failed to load entry point: {}", e))?;
self.lua = Some(lua);
Ok(())
@@ -89,7 +91,9 @@ impl LoadedPlugin {
/// Get provider registrations from this plugin
pub fn get_provider_registrations(&self) -> Result<Vec<ProviderRegistration>, String> {
let lua = self.lua.as_ref()
let lua = self
.lua
.as_ref()
.ok_or_else(|| "Plugin not initialized".to_string())?;
api::get_provider_registrations(lua)
@@ -98,25 +102,33 @@ impl LoadedPlugin {
/// Call a provider's refresh function
pub fn call_provider_refresh(&self, provider_name: &str) -> Result<Vec<PluginItem>, String> {
let lua = self.lua.as_ref()
let lua = self
.lua
.as_ref()
.ok_or_else(|| "Plugin not initialized".to_string())?;
api::call_refresh(lua, provider_name)
.map_err(|e| format!("Refresh failed: {}", e))
api::call_refresh(lua, provider_name).map_err(|e| format!("Refresh failed: {}", e))
}
/// Call a provider's query function
pub fn call_provider_query(&self, provider_name: &str, query: &str) -> Result<Vec<PluginItem>, String> {
let lua = self.lua.as_ref()
pub fn call_provider_query(
&self,
provider_name: &str,
query: &str,
) -> Result<Vec<PluginItem>, String> {
let lua = self
.lua
.as_ref()
.ok_or_else(|| "Plugin not initialized".to_string())?;
api::call_query(lua, provider_name, query)
.map_err(|e| format!("Query failed: {}", e))
api::call_query(lua, provider_name, query).map_err(|e| format!("Query failed: {}", e))
}
}
/// Discover plugins in a directory
pub fn discover_plugins(plugins_dir: &Path) -> Result<HashMap<String, (PluginManifest, PathBuf)>, String> {
pub fn discover_plugins(
plugins_dir: &Path,
) -> Result<HashMap<String, (PluginManifest, PathBuf)>, String> {
let mut plugins = HashMap::new();
if !plugins_dir.exists() {
@@ -146,13 +158,21 @@ pub fn discover_plugins(plugins_dir: &Path) -> Result<HashMap<String, (PluginMan
Ok(manifest) => {
let id = manifest.plugin.id.clone();
if plugins.contains_key(&id) {
eprintln!("owlry-lua: Duplicate plugin ID '{}', skipping {}", id, path.display());
eprintln!(
"owlry-lua: Duplicate plugin ID '{}', skipping {}",
id,
path.display()
);
continue;
}
plugins.insert(id, (manifest, path));
}
Err(e) => {
eprintln!("owlry-lua: Failed to load plugin at {}: {}", path.display(), e);
eprintln!(
"owlry-lua: Failed to load plugin at {}: {}",
path.display(),
e
);
}
}
}

View File

@@ -90,10 +90,10 @@ pub struct PluginPermissions {
impl PluginManifest {
/// Load a plugin manifest from a plugin.toml file
pub fn load(path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: PluginManifest = toml::from_str(&content)
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
let content =
std::fs::read_to_string(path).map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: PluginManifest =
toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
manifest.validate()?;
Ok(manifest)
}
@@ -105,7 +105,12 @@ impl PluginManifest {
return Err("Plugin ID cannot be empty".to_string());
}
if !self.plugin.id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
if !self
.plugin
.id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err("Plugin ID must be lowercase alphanumeric with hyphens".to_string());
}
@@ -116,7 +121,10 @@ impl PluginManifest {
// Validate owlry_version constraint
if semver::VersionReq::parse(&self.plugin.owlry_version).is_err() {
return Err(format!("Invalid owlry_version constraint: {}", self.plugin.owlry_version));
return Err(format!(
"Invalid owlry_version constraint: {}",
self.plugin.owlry_version
));
}
Ok(())

View File

@@ -28,7 +28,7 @@ impl Default for SandboxConfig {
allow_commands: false,
allow_network: false,
allow_external_fs: false,
max_run_time_ms: 5000, // 5 seconds
max_run_time_ms: 5000, // 5 seconds
max_memory: 64 * 1024 * 1024, // 64 MB
}
}
@@ -50,11 +50,7 @@ impl SandboxConfig {
pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> {
// Create Lua with safe standard libraries only
// We exclude: debug, io, os (dangerous parts), package (loadlib), ffi
let libs = StdLib::COROUTINE
| StdLib::TABLE
| StdLib::STRING
| StdLib::UTF8
| StdLib::MATH;
let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
let lua = Lua::new_with(libs, mlua::LuaOptions::default())?;
@@ -74,11 +70,15 @@ fn setup_safe_globals(lua: &Lua) -> LuaResult<()> {
// Create a restricted os table with only safe functions
let os_table = lua.create_table()?;
os_table.set("clock", lua.create_function(|_, ()| {
Ok(std::time::Instant::now().elapsed().as_secs_f64())
})?)?;
os_table.set(
"clock",
lua.create_function(|_, ()| Ok(std::time::Instant::now().elapsed().as_secs_f64()))?,
)?;
os_table.set("date", lua.create_function(os_date)?)?;
os_table.set("difftime", lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?)?;
os_table.set(
"difftime",
lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?,
)?;
os_table.set("time", lua.create_function(os_time)?)?;
globals.set("os", os_table)?;
@@ -107,8 +107,7 @@ fn os_time(_lua: &Lua, _args: ()) -> LuaResult<i64> {
/// Load and run a Lua file in the given runtime
pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> {
let content = std::fs::read_to_string(path)
.map_err(mlua::Error::external)?;
let content = std::fs::read_to_string(path).map_err(mlua::Error::external)?;
lua.load(&content)
.set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk"))
.into_function()?

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-plugin-api"
version = "0.4.10"
version = "1.0.0"
edition.workspace = true
rust-version.workspace = true
license.workspace = true

View File

@@ -284,12 +284,8 @@ pub enum NotifyUrgency {
pub struct HostAPI {
/// Send a notification to the user
/// Parameters: summary, body, icon (optional, empty string for none), urgency
pub notify: extern "C" fn(
summary: RStr<'_>,
body: RStr<'_>,
icon: RStr<'_>,
urgency: NotifyUrgency,
),
pub notify:
extern "C" fn(summary: RStr<'_>, body: RStr<'_>, icon: RStr<'_>, urgency: NotifyUrgency),
/// Log a message at info level
pub log_info: extern "C" fn(message: RStr<'_>),

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry-rune"
version = "0.4.10"
version = "1.0.0"
edition = "2024"
rust-version = "1.90"
description = "Rune scripting runtime for owlry plugins"

View File

@@ -75,7 +75,11 @@ pub struct RuneRuntimeVTable {
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<RuneProviderInfo>,
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
pub query: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem>,
pub query: extern "C" fn(
handle: RuntimeHandle,
provider_id: RStr<'_>,
query: RStr<'_>,
) -> RVec<PluginItem>,
pub drop: extern "C" fn(handle: RuntimeHandle),
}
@@ -94,7 +98,10 @@ extern "C" fn runtime_init(plugins_dir: RStr<'_>) -> RuntimeHandle {
let _ = env_logger::try_init();
let plugins_dir = PathBuf::from(plugins_dir.as_str());
log::info!("Initializing Rune runtime with plugins from: {}", plugins_dir.display());
log::info!(
"Initializing Rune runtime with plugins from: {}",
plugins_dir.display()
);
let mut state = RuntimeState {
plugins: HashMap::new(),
@@ -113,15 +120,20 @@ extern "C" fn runtime_init(plugins_dir: RStr<'_>) -> RuntimeHandle {
type_id: RString::from(reg.type_id.as_str()),
default_icon: RString::from(reg.default_icon.as_str()),
is_static: reg.is_static,
prefix: reg.prefix.as_ref()
prefix: reg
.prefix
.as_ref()
.map(|p| RString::from(p.as_str()))
.into(),
});
}
state.plugins.insert(id, plugin);
}
log::info!("Loaded {} Rune plugin(s) with {} provider(s)",
state.plugins.len(), state.providers.len());
log::info!(
"Loaded {} Rune plugin(s) with {} provider(s)",
state.plugins.len(),
state.providers.len()
);
}
Err(e) => {
log::error!("Failed to discover Rune plugins: {}", e);

View File

@@ -8,7 +8,7 @@ use rune::{Context, Unit};
use crate::api::{self, ProviderRegistration};
use crate::manifest::PluginManifest;
use crate::runtime::{compile_source, create_context, create_vm, SandboxConfig};
use crate::runtime::{SandboxConfig, compile_source, create_context, create_vm};
use owlry_plugin_api::PluginItem;
@@ -29,8 +29,8 @@ impl LoadedPlugin {
/// Create and initialize a new plugin
pub fn new(manifest: PluginManifest, path: PathBuf) -> Result<Self, String> {
let sandbox = SandboxConfig::from_permissions(&manifest.permissions);
let context = create_context(&sandbox)
.map_err(|e| format!("Failed to create context: {}", e))?;
let context =
create_context(&sandbox).map_err(|e| format!("Failed to create context: {}", e))?;
let entry_path = path.join(&manifest.plugin.entry);
if !entry_path.exists() {
@@ -45,15 +45,14 @@ impl LoadedPlugin {
.map_err(|e| format!("Failed to compile: {}", e))?;
// Run the entry point to register providers
let mut vm = create_vm(&context, unit.clone())
.map_err(|e| format!("Failed to create VM: {}", e))?;
let mut vm =
create_vm(&context, unit.clone()).map_err(|e| format!("Failed to create VM: {}", e))?;
// Execute the main function if it exists
match vm.call(rune::Hash::type_hash(["main"]), ()) {
Ok(result) => {
// Try to complete the execution
let _: () = rune::from_value(result)
.unwrap_or(());
let _: () = rune::from_value(result).unwrap_or(());
}
Err(_) => {
// No main function is okay
@@ -111,7 +110,10 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result<HashMap<String, Loade
let mut plugins = HashMap::new();
if !plugins_dir.exists() {
log::debug!("Plugins directory does not exist: {}", plugins_dir.display());
log::debug!(
"Plugins directory does not exist: {}",
plugins_dir.display()
);
return Ok(plugins);
}
@@ -135,7 +137,11 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result<HashMap<String, Loade
let manifest = match PluginManifest::load(&manifest_path) {
Ok(m) => m,
Err(e) => {
log::warn!("Failed to load manifest at {}: {}", manifest_path.display(), e);
log::warn!(
"Failed to load manifest at {}: {}",
manifest_path.display(),
e
);
continue;
}
};

View File

@@ -64,10 +64,10 @@ pub struct PluginPermissions {
impl PluginManifest {
/// Load manifest from a plugin.toml file
pub fn load(path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(path)
.map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: PluginManifest = toml::from_str(&content)
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
let content =
std::fs::read_to_string(path).map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: PluginManifest =
toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
manifest.validate()?;
Ok(manifest)
}
@@ -78,7 +78,12 @@ impl PluginManifest {
return Err("Plugin ID cannot be empty".to_string());
}
if !self.plugin.id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
if !self
.plugin
.id
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
{
return Err("Plugin ID must be lowercase alphanumeric with hyphens".to_string());
}

View File

@@ -25,7 +25,6 @@ pub struct SandboxConfig {
pub allowed_commands: Vec<String>,
}
impl SandboxConfig {
/// Create sandbox config from plugin permissions
pub fn from_permissions(permissions: &PluginPermissions) -> Self {
@@ -59,12 +58,9 @@ pub fn create_context(sandbox: &SandboxConfig) -> Result<Context, rune::ContextE
}
/// Compile Rune source code into a Unit
pub fn compile_source(
context: &Context,
source_path: &Path,
) -> Result<Arc<Unit>, CompileError> {
let source_content = std::fs::read_to_string(source_path)
.map_err(|e| CompileError::Io(e.to_string()))?;
pub fn compile_source(context: &Context, source_path: &Path) -> Result<Arc<Unit>, CompileError> {
let source_content =
std::fs::read_to_string(source_path).map_err(|e| CompileError::Io(e.to_string()))?;
let source_name = source_path
.file_name()
@@ -73,7 +69,10 @@ pub fn compile_source(
let mut sources = Sources::new();
sources
.insert(Source::new(source_name, &source_content).map_err(|e| CompileError::Compile(e.to_string()))?)
.insert(
Source::new(source_name, &source_content)
.map_err(|e| CompileError::Compile(e.to_string()))?,
)
.map_err(|e| CompileError::Compile(format!("Failed to insert source: {}", e)))?;
let mut diagnostics = Diagnostics::new();
@@ -97,13 +96,11 @@ pub fn compile_source(
}
/// Create a new Rune VM from compiled unit
pub fn create_vm(
context: &Context,
unit: Arc<Unit>,
) -> Result<Vm, CompileError> {
pub fn create_vm(context: &Context, unit: Arc<Unit>) -> Result<Vm, CompileError> {
let runtime = Arc::new(
context.runtime()
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))?
context
.runtime()
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))?,
);
Ok(Vm::new(runtime, unit))
}

View File

@@ -1,6 +1,6 @@
[package]
name = "owlry"
version = "0.4.10"
version = "1.0.0"
edition = "2024"
rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland"

View File

@@ -4,15 +4,15 @@ use crate::client::CoreClient;
use crate::providers::DmenuProvider;
use crate::theme;
use crate::ui::MainWindow;
use gtk4::prelude::*;
use gtk4::{Application, CssProvider, gio};
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use log::{debug, info, warn};
use owlry_core::config::Config;
use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter;
use owlry_core::paths;
use owlry_core::providers::{Provider, ProviderManager, ProviderType};
use gtk4::prelude::*;
use gtk4::{gio, Application, CssProvider};
use gtk4_layer_shell::{Edge, Layer, LayerShell};
use log::{debug, info, warn};
use std::cell::RefCell;
use std::rc::Rc;
@@ -61,7 +61,7 @@ impl OwlryApp {
let frecency = FrecencyStore::load_or_default();
SearchBackend::Local {
providers: provider_manager,
providers: Box::new(provider_manager),
frecency,
}
} else {
@@ -98,11 +98,7 @@ impl OwlryApp {
&config.borrow().providers,
)
} else {
ProviderFilter::new(
None,
Some(provider_types),
&config.borrow().providers,
)
ProviderFilter::new(None, Some(provider_types), &config.borrow().providers)
}
} else {
ProviderFilter::new(None, None, &config.borrow().providers)
@@ -180,7 +176,7 @@ impl OwlryApp {
let frecency = FrecencyStore::load_or_default();
SearchBackend::Local {
providers: provider_manager,
providers: Box::new(provider_manager),
frecency,
}
}
@@ -241,16 +237,17 @@ impl OwlryApp {
// 3. Load user's custom stylesheet if exists
if let Some(custom_path) = paths::custom_style_file()
&& custom_path.exists() {
let custom_provider = CssProvider::new();
custom_provider.load_from_path(&custom_path);
gtk4::style_context_add_provider_for_display(
&display,
&custom_provider,
gtk4::STYLE_PROVIDER_PRIORITY_USER,
);
debug!("Loaded custom CSS from {:?}", custom_path);
}
&& custom_path.exists()
{
let custom_provider = CssProvider::new();
custom_provider.load_from_path(&custom_path);
gtk4::style_context_add_provider_for_display(
&display,
&custom_provider,
gtk4::STYLE_PROVIDER_PRIORITY_USER,
);
debug!("Loaded custom CSS from {:?}", custom_path);
}
// 4. Inject config variables (highest priority for overrides)
let vars_css = theme::generate_variables_css(&config.appearance);

View File

@@ -4,12 +4,12 @@
//! In dmenu mode, the UI uses a local ProviderManager directly (no daemon).
use crate::client::CoreClient;
use log::warn;
use owlry_core::config::Config;
use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter;
use owlry_core::ipc::ResultItem;
use owlry_core::providers::{LaunchItem, ProviderManager, ProviderType};
use owlry_core::data::FrecencyStore;
use owlry_core::config::Config;
use log::warn;
/// Backend for search operations. Wraps either an IPC client (daemon mode)
/// or a local ProviderManager (dmenu mode).
@@ -18,7 +18,7 @@ pub enum SearchBackend {
Daemon(CoreClient),
/// Direct local provider manager (dmenu mode only)
Local {
providers: ProviderManager,
providers: Box<ProviderManager>,
frecency: FrecencyStore,
},
}
@@ -64,7 +64,14 @@ impl SearchBackend {
if use_frecency {
providers
.search_with_frecency(query, max_results, filter, frecency, frecency_weight, None)
.search_with_frecency(
query,
max_results,
filter,
frecency,
frecency_weight,
None,
)
.into_iter()
.map(|(item, _)| item)
.collect()
@@ -123,7 +130,14 @@ impl SearchBackend {
if use_frecency {
providers
.search_with_frecency(query, max_results, filter, frecency, frecency_weight, tag_filter)
.search_with_frecency(
query,
max_results,
filter,
frecency,
frecency_weight,
tag_filter,
)
.into_iter()
.map(|(item, _)| item)
.collect()
@@ -141,18 +155,14 @@ impl SearchBackend {
/// Execute a plugin action command. Returns true if handled.
pub fn execute_plugin_action(&mut self, command: &str) -> bool {
match self {
SearchBackend::Daemon(client) => {
match client.plugin_action(command) {
Ok(handled) => handled,
Err(e) => {
warn!("IPC plugin_action failed: {}", e);
false
}
SearchBackend::Daemon(client) => match client.plugin_action(command) {
Ok(handled) => handled,
Err(e) => {
warn!("IPC plugin_action failed: {}", e);
false
}
}
SearchBackend::Local { providers, .. } => {
providers.execute_plugin_action(command)
}
},
SearchBackend::Local { providers, .. } => providers.execute_plugin_action(command),
}
}
@@ -165,20 +175,18 @@ impl SearchBackend {
display_name: &str,
) -> Option<(String, Vec<LaunchItem>)> {
match self {
SearchBackend::Daemon(client) => {
match client.submenu(plugin_id, data) {
Ok(items) if !items.is_empty() => {
let actions: Vec<LaunchItem> =
items.into_iter().map(result_to_launch_item).collect();
Some((display_name.to_string(), actions))
}
Ok(_) => None,
Err(e) => {
warn!("IPC submenu query failed: {}", e);
None
}
SearchBackend::Daemon(client) => match client.submenu(plugin_id, data) {
Ok(items) if !items.is_empty() => {
let actions: Vec<LaunchItem> =
items.into_iter().map(result_to_launch_item).collect();
Some((display_name.to_string(), actions))
}
}
Ok(_) => None,
Err(e) => {
warn!("IPC submenu query failed: {}", e);
None
}
},
SearchBackend::Local { providers, .. } => {
providers.query_submenu_actions(plugin_id, data, display_name)
}
@@ -218,22 +226,18 @@ impl SearchBackend {
#[allow(dead_code)]
pub fn available_provider_ids(&mut self) -> Vec<String> {
match self {
SearchBackend::Daemon(client) => {
match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
Err(e) => {
warn!("IPC providers query failed: {}", e);
Vec::new()
}
SearchBackend::Daemon(client) => match client.providers() {
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
Err(e) => {
warn!("IPC providers query failed: {}", e);
Vec::new()
}
}
SearchBackend::Local { providers, .. } => {
providers
.available_providers()
.into_iter()
.map(|d| d.id)
.collect()
}
},
SearchBackend::Local { providers, .. } => providers
.available_providers()
.into_iter()
.map(|d| d.id)
.collect(),
}
}
}

View File

@@ -41,20 +41,14 @@ impl CoreClient {
.args(["--user", "start", "owlry-core"])
.status()
.map_err(|e| {
io::Error::new(
io::ErrorKind::Other,
format!("failed to start owlry-core via systemd: {e}"),
)
io::Error::other(format!("failed to start owlry-core via systemd: {e}"))
})?;
if !status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!(
"systemctl --user start owlry-core exited with status {}",
status
),
));
return Err(io::Error::other(format!(
"systemctl --user start owlry-core exited with status {}",
status
)));
}
// Retry with exponential backoff.
@@ -66,9 +60,7 @@ impl CoreClient {
Err(e) if i == delays.len() - 1 => {
return Err(io::Error::new(
io::ErrorKind::ConnectionRefused,
format!(
"daemon started but socket not available after retries: {e}"
),
format!("daemon started but socket not available after retries: {e}"),
));
}
Err(_) => continue,
@@ -87,11 +79,7 @@ impl CoreClient {
}
/// Send a search query and return matching results.
pub fn query(
&mut self,
text: &str,
modes: Option<Vec<String>>,
) -> io::Result<Vec<ResultItem>> {
pub fn query(&mut self, text: &str, modes: Option<Vec<String>>) -> io::Result<Vec<ResultItem>> {
self.send(&Request::Query {
text: text.to_string(),
modes,
@@ -99,9 +87,7 @@ impl CoreClient {
match self.receive()? {
Response::Results { items } => Ok(items),
Response::Error { message } => {
Err(io::Error::new(io::ErrorKind::Other, message))
}
Response::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected response to Query: {other:?}"),
@@ -118,9 +104,7 @@ impl CoreClient {
match self.receive()? {
Response::Ack => Ok(()),
Response::Error { message } => {
Err(io::Error::new(io::ErrorKind::Other, message))
}
Response::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected response to Launch: {other:?}"),
@@ -134,9 +118,7 @@ impl CoreClient {
match self.receive()? {
Response::Providers { list } => Ok(list),
Response::Error { message } => {
Err(io::Error::new(io::ErrorKind::Other, message))
}
Response::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected response to Providers: {other:?}"),
@@ -150,9 +132,7 @@ impl CoreClient {
match self.receive()? {
Response::Ack => Ok(()),
Response::Error { message } => {
Err(io::Error::new(io::ErrorKind::Other, message))
}
Response::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected response to Toggle: {other:?}"),
@@ -178,11 +158,7 @@ impl CoreClient {
}
/// Query a plugin's submenu actions.
pub fn submenu(
&mut self,
plugin_id: &str,
data: &str,
) -> io::Result<Vec<ResultItem>> {
pub fn submenu(&mut self, plugin_id: &str, data: &str) -> io::Result<Vec<ResultItem>> {
self.send(&Request::Submenu {
plugin_id: plugin_id.to_string(),
data: data.to_string(),
@@ -190,9 +166,7 @@ impl CoreClient {
match self.receive()? {
Response::SubmenuItems { items } => Ok(items),
Response::Error { message } => {
Err(io::Error::new(io::ErrorKind::Other, message))
}
Response::Error { message } => Err(io::Error::other(message)),
other => Err(io::Error::new(
io::ErrorKind::InvalidData,
format!("unexpected response to Submenu: {other:?}"),
@@ -220,8 +194,7 @@ impl CoreClient {
"daemon closed the connection",
));
}
serde_json::from_str(line.trim())
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
serde_json::from_str(line.trim()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}
}
@@ -239,11 +212,7 @@ mod tests {
/// socket path to avoid collisions when tests run in parallel.
fn mock_server(response: Response) -> PathBuf {
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir = std::env::temp_dir().join(format!(
"owlry-test-{}-{}",
std::process::id(),
n
));
let dir = std::env::temp_dir().join(format!("owlry-test-{}-{}", std::process::id(), n));
let _ = std::fs::create_dir_all(&dir);
let sock = dir.join("test.sock");
let _ = std::fs::remove_file(&sock);

View File

@@ -1,7 +1,7 @@
mod app;
mod backend;
pub mod client;
mod cli;
pub mod client;
mod plugin_commands;
mod providers;
mod theme;
@@ -65,7 +65,11 @@ fn main() {
}
// No subcommand - launch the app
let default_level = if cfg!(feature = "dev-logging") { "debug" } else { "info" };
let default_level = if cfg!(feature = "dev-logging") {
"debug"
} else {
"info"
};
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level))
.format_timestamp_millis()

View File

@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
use crate::cli::{PluginCommand as CliPluginCommand, PluginRuntime};
use owlry_core::config::Config;
use owlry_core::paths;
use owlry_core::plugins::manifest::{discover_plugins, PluginManifest};
use owlry_core::plugins::manifest::{PluginManifest, discover_plugins};
use owlry_core::plugins::registry::{self, RegistryClient};
use owlry_core::plugins::runtime_loader::{lua_runtime_available, rune_runtime_available};
@@ -46,15 +46,30 @@ fn any_runtime_available() -> bool {
/// Execute a plugin command
pub fn execute(cmd: CliPluginCommand) -> CommandResult {
match cmd {
CliPluginCommand::List { enabled, disabled, runtime, available, refresh, json } => {
CliPluginCommand::List {
enabled,
disabled,
runtime,
available,
refresh,
json,
} => {
if available {
cmd_list_available(refresh, json)
} else {
cmd_list_installed(enabled, disabled, runtime, json)
}
}
CliPluginCommand::Search { query, refresh, json } => cmd_search(&query, refresh, json),
CliPluginCommand::Info { name, registry, json } => {
CliPluginCommand::Search {
query,
refresh,
json,
} => cmd_search(&query, refresh, json),
CliPluginCommand::Info {
name,
registry,
json,
} => {
if registry {
cmd_info_registry(&name, json)
} else {
@@ -74,15 +89,29 @@ pub fn execute(cmd: CliPluginCommand) -> CommandResult {
CliPluginCommand::Update { name } => cmd_update(name.as_deref()),
CliPluginCommand::Enable { name } => cmd_enable(&name),
CliPluginCommand::Disable { name } => cmd_disable(&name),
CliPluginCommand::Create { name, runtime, dir, display_name, description } => {
CliPluginCommand::Create {
name,
runtime,
dir,
display_name,
description,
} => {
check_runtime_available(runtime)?;
cmd_create(&name, runtime, dir.as_deref(), display_name.as_deref(), description.as_deref())
cmd_create(
&name,
runtime,
dir.as_deref(),
display_name.as_deref(),
description.as_deref(),
)
}
CliPluginCommand::Validate { path } => cmd_validate(path.as_deref()),
CliPluginCommand::Runtimes => cmd_runtimes(),
CliPluginCommand::Run { plugin_id, command, args } => {
cmd_run_plugin_command(&plugin_id, &command, &args)
}
CliPluginCommand::Run {
plugin_id,
command,
args,
} => cmd_run_plugin_command(&plugin_id, &command, &args),
CliPluginCommand::Commands { plugin_id } => cmd_list_commands(plugin_id.as_deref()),
}
}
@@ -351,7 +380,10 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
});
println!("{}", serde_json::to_string_pretty(&info).unwrap());
} else {
println!("Plugin: {} v{}", manifest.plugin.name, manifest.plugin.version);
println!(
"Plugin: {} v{}",
manifest.plugin.name, manifest.plugin.version
);
println!("ID: {}", manifest.plugin.id);
if !manifest.plugin.description.is_empty() {
println!("Description: {}", manifest.plugin.description);
@@ -359,11 +391,18 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
if !manifest.plugin.author.is_empty() {
println!("Author: {}", manifest.plugin.author);
}
println!("Status: {}", if is_enabled { "enabled" } else { "disabled" });
println!(
"Status: {}",
if is_enabled { "enabled" } else { "disabled" }
);
println!(
"Runtime: {}{}",
runtime,
if runtime_available { "" } else { " (NOT INSTALLED)" }
if runtime_available {
""
} else {
" (NOT INSTALLED)"
}
);
println!("Path: {}", plugin_path.display());
println!();
@@ -382,12 +421,25 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
}
println!();
println!("Permissions:");
println!(" Network: {}", if manifest.permissions.network { "yes" } else { "no" });
println!(
" Network: {}",
if manifest.permissions.network {
"yes"
} else {
"no"
}
);
if !manifest.permissions.filesystem.is_empty() {
println!(" Filesystem: {}", manifest.permissions.filesystem.join(", "));
println!(
" Filesystem: {}",
manifest.permissions.filesystem.join(", ")
);
}
if !manifest.permissions.run_commands.is_empty() {
println!(" Commands: {}", manifest.permissions.run_commands.join(", "));
println!(
" Commands: {}",
manifest.permissions.run_commands.join(", ")
);
}
}
@@ -398,7 +450,8 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
fn cmd_info_registry(name: &str, json_output: bool) -> CommandResult {
let client = get_registry_client();
let plugin = client.find(name, false)?
let plugin = client
.find(name, false)?
.ok_or_else(|| format!("Plugin '{}' not found in registry", name))?;
if json_output {
@@ -466,12 +519,10 @@ fn cmd_install(source: &str, force: bool) -> CommandResult {
println!("Found: {} v{}", plugin.name, plugin.version);
install_from_git(&plugin.repository, &plugins_dir, force)
}
None => {
Err(format!(
"Plugin '{}' not found in registry. Use a local path or git URL.",
source
))
}
None => Err(format!(
"Plugin '{}' not found in registry. Use a local path or git URL.",
source
)),
}
}
}
@@ -597,8 +648,7 @@ fn cmd_remove(name: &str, yes: bool) -> CommandResult {
}
}
fs::remove_dir_all(&plugin_path)
.map_err(|e| format!("Failed to remove plugin: {}", e))?;
fs::remove_dir_all(&plugin_path).map_err(|e| format!("Failed to remove plugin: {}", e))?;
// Also remove from disabled list if present
if let Ok(mut config) = Config::load() {
@@ -645,7 +695,9 @@ fn cmd_enable(name: &str) -> CommandResult {
}
config.plugins.disabled_plugins.retain(|id| id != name);
config.save().map_err(|e| format!("Failed to save config: {}", e))?;
config
.save()
.map_err(|e| format!("Failed to save config: {}", e))?;
println!("Enabled plugin '{}'", name);
Ok(())
@@ -668,7 +720,9 @@ fn cmd_disable(name: &str) -> CommandResult {
}
config.plugins.disabled_plugins.push(name.to_string());
config.save().map_err(|e| format!("Failed to save config: {}", e))?;
config
.save()
.map_err(|e| format!("Failed to save config: {}", e))?;
println!("Disabled plugin '{}'", name);
Ok(())
@@ -688,11 +742,13 @@ fn cmd_create(
let plugin_dir = base_dir.join(name);
if plugin_dir.exists() {
return Err(format!("Directory '{}' already exists", plugin_dir.display()));
return Err(format!(
"Directory '{}' already exists",
plugin_dir.display()
));
}
fs::create_dir_all(&plugin_dir)
.map_err(|e| format!("Failed to create directory: {}", e))?;
fs::create_dir_all(&plugin_dir).map_err(|e| format!("Failed to create directory: {}", e))?;
let display = display_name.unwrap_or(name);
let desc = description.unwrap_or("A custom owlry plugin");
@@ -825,14 +881,28 @@ pub fn register(owlry) {{{{
}
}
println!("Created {} plugin '{}' at {}", runtime, name, plugin_dir.display());
println!(
"Created {} plugin '{}' at {}",
runtime,
name,
plugin_dir.display()
);
println!();
println!("Next steps:");
println!(" 1. Edit {}/{} to implement your provider", name, entry_file);
println!(" 2. Install: owlry plugin install {}", plugin_dir.display());
println!(
" 1. Edit {}/{} to implement your provider",
name, entry_file
);
println!(
" 2. Install: owlry plugin install {}",
plugin_dir.display()
);
println!(" 3. Test: owlry (your plugin items should appear)");
println!();
println!("Runtime: {} (requires owlry-{} package)", runtime, entry_ext);
println!(
"Runtime: {} (requires owlry-{} package)",
runtime, entry_ext
);
Ok(())
}
@@ -996,15 +1066,29 @@ fn cmd_run_plugin_command(plugin_id: &str, command: &str, args: &[String]) -> Co
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
// Check if plugin provides this command
let cmd_info = manifest.provides.commands.iter().find(|c| c.name == command);
let cmd_info = manifest
.provides
.commands
.iter()
.find(|c| c.name == command);
if cmd_info.is_none() {
let available: Vec<_> = manifest.provides.commands.iter().map(|c| c.name.as_str()).collect();
let available: Vec<_> = manifest
.provides
.commands
.iter()
.map(|c| c.name.as_str())
.collect();
if available.is_empty() {
return Err(format!("Plugin '{}' does not provide any CLI commands", plugin_id));
return Err(format!(
"Plugin '{}' does not provide any CLI commands",
plugin_id
));
}
return Err(format!(
"Plugin '{}' does not have command '{}'. Available: {}",
plugin_id, command, available.join(", ")
plugin_id,
command,
available.join(", ")
));
}
@@ -1030,10 +1114,8 @@ fn execute_plugin_command(
// Load the appropriate runtime
let loaded_runtime = match runtime {
PluginRuntime::Lua => {
LoadedRuntime::load_lua(plugin_path.parent().unwrap_or(plugin_path))
.map_err(|e| format!("Failed to load Lua runtime: {}", e))?
}
PluginRuntime::Lua => LoadedRuntime::load_lua(plugin_path.parent().unwrap_or(plugin_path))
.map_err(|e| format!("Failed to load Lua runtime: {}", e))?,
PluginRuntime::Rune => {
LoadedRuntime::load_rune(plugin_path.parent().unwrap_or(plugin_path))
.map_err(|e| format!("Failed to load Rune runtime: {}", e))?
@@ -1047,7 +1129,10 @@ fn execute_plugin_command(
let _query = query_parts.join(":");
// Find the provider from this plugin and send the command query
let _provider_name = manifest.provides.providers.first()
let _provider_name = manifest
.provides
.providers
.first()
.ok_or_else(|| format!("Plugin '{}' has no providers", manifest.plugin.id))?;
// Query the provider with the command
@@ -1056,14 +1141,31 @@ fn execute_plugin_command(
// For now, we use a simpler approach: invoke the entry point with command args
// This requires runtime support for command execution
println!("Executing: owlry plugin run {} {} {}", manifest.plugin.id, command, args.join(" "));
println!(
"Executing: owlry plugin run {} {} {}",
manifest.plugin.id,
command,
args.join(" ")
);
println!();
println!("Note: Plugin command execution requires runtime support.");
println!("The plugin entry point should handle CLI commands via owlry.command.register()");
println!();
println!("Runtime: {} ({})", runtime, if PathBuf::from(SYSTEM_RUNTIMES_DIR).join(
match runtime { PluginRuntime::Lua => "liblua.so", PluginRuntime::Rune => "librune.so" }
).exists() { "available" } else { "NOT INSTALLED" });
println!(
"Runtime: {} ({})",
runtime,
if PathBuf::from(SYSTEM_RUNTIMES_DIR)
.join(match runtime {
PluginRuntime::Lua => "liblua.so",
PluginRuntime::Rune => "librune.so",
})
.exists()
{
"available"
} else {
"NOT INSTALLED"
}
);
// TODO: Implement actual command execution through runtime
// This would involve:
@@ -1087,7 +1189,8 @@ fn cmd_list_commands(plugin_id: Option<&str>) -> CommandResult {
if let Some(id) = plugin_id {
// Show commands for a specific plugin
let (manifest, _path) = discovered.get(id)
let (manifest, _path) = discovered
.get(id)
.ok_or_else(|| format!("Plugin '{}' not found", id))?;
if manifest.provides.commands.is_empty() {

View File

@@ -1,5 +1,5 @@
use owlry_core::providers::{LaunchItem, Provider, ProviderType};
use log::debug;
use owlry_core::providers::{LaunchItem, Provider, ProviderType};
use std::io::{self, BufRead};
/// Provider for dmenu-style input from stdin

View File

@@ -6,7 +6,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
// Always inject layout config values
css.push_str(&format!(" --owlry-font-size: {}px;\n", config.font_size));
css.push_str(&format!(" --owlry-border-radius: {}px;\n", config.border_radius));
css.push_str(&format!(
" --owlry-border-radius: {}px;\n",
config.border_radius
));
// Only inject colors if user specified them
if let Some(ref bg) = config.colors.background {
@@ -22,7 +25,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
css.push_str(&format!(" --owlry-text: {};\n", text));
}
if let Some(ref text_secondary) = config.colors.text_secondary {
css.push_str(&format!(" --owlry-text-secondary: {};\n", text_secondary));
css.push_str(&format!(
" --owlry-text-secondary: {};\n",
text_secondary
));
}
if let Some(ref accent) = config.colors.accent {
css.push_str(&format!(" --owlry-accent: {};\n", accent));
@@ -36,7 +42,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
css.push_str(&format!(" --owlry-badge-app: {};\n", badge_app));
}
if let Some(ref badge_bookmark) = config.colors.badge_bookmark {
css.push_str(&format!(" --owlry-badge-bookmark: {};\n", badge_bookmark));
css.push_str(&format!(
" --owlry-badge-bookmark: {};\n",
badge_bookmark
));
}
if let Some(ref badge_calc) = config.colors.badge_calc {
css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc));

View File

@@ -1,9 +1,6 @@
use crate::backend::SearchBackend;
use owlry_core::config::Config;
use owlry_core::filter::ProviderFilter;
use owlry_core::providers::{LaunchItem, ProviderType};
use crate::ui::submenu;
use crate::ui::ResultRow;
use crate::ui::submenu;
use gtk4::gdk::Key;
use gtk4::prelude::*;
use gtk4::{
@@ -11,6 +8,9 @@ use gtk4::{
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
};
use log::info;
use owlry_core::config::Config;
use owlry_core::filter::ProviderFilter;
use owlry_core::providers::{LaunchItem, ProviderType};
#[cfg(feature = "dev-logging")]
use log::debug;
@@ -148,7 +148,9 @@ impl MainWindow {
header_box.append(&filter_tabs);
// Search entry with dynamic placeholder (or custom prompt if provided)
let placeholder = custom_prompt.clone().unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
let placeholder = custom_prompt
.clone()
.unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
let search_entry = Entry::builder()
.placeholder_text(&placeholder)
.hexpand(true)
@@ -293,8 +295,16 @@ impl MainWindow {
// Show number hint in the label for first 9 tabs (using superscript)
let label = if idx < 9 {
let superscript = match idx + 1 {
1 => "¹", 2 => "²", 3 => "³", 4 => "", 5 => "",
6 => "", 7 => "", 8 => "", 9 => "", _ => "",
1 => "¹",
2 => "²",
3 => "³",
4 => "",
5 => "",
6 => "",
7 => "",
8 => "",
9 => "",
_ => "",
};
format!("{}{}", base_label, superscript)
} else {
@@ -494,7 +504,11 @@ impl MainWindow {
actions: Vec<LaunchItem>,
) {
#[cfg(feature = "dev-logging")]
debug!("[UI] Entering submenu: {} ({} actions)", display_name, actions.len());
debug!(
"[UI] Entering submenu: {} ({} actions)",
display_name,
actions.len()
);
// Save current state
{
@@ -705,7 +719,8 @@ impl MainWindow {
}
// current_results holds only what's displayed (for selection/activation)
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
*current_results.borrow_mut() =
results.into_iter().take(initial_count).collect();
},
);
@@ -736,15 +751,19 @@ impl MainWindow {
if let Some(item) = results.get(index) {
// Check if this is a submenu item and query the plugin for actions
let submenu_result = if submenu::is_submenu_item(item) {
if let Some((plugin_id, data)) = submenu::parse_submenu_command(&item.command) {
if let Some((plugin_id, data)) =
submenu::parse_submenu_command(&item.command)
{
// Clone values before dropping borrow
let plugin_id = plugin_id.to_string();
let data = data.to_string();
let display_name = item.name.clone();
drop(results); // Release borrow before querying
backend_for_activate
.borrow_mut()
.query_submenu_actions(&plugin_id, &data, &display_name)
backend_for_activate.borrow_mut().query_submenu_actions(
&plugin_id,
&data,
&display_name,
)
} else {
drop(results);
None
@@ -843,7 +862,10 @@ impl MainWindow {
let shift = modifiers.contains(gtk4::gdk::ModifierType::SHIFT_MASK);
#[cfg(feature = "dev-logging")]
debug!("[UI] Key pressed: {:?} (ctrl={}, shift={})", key, ctrl, shift);
debug!(
"[UI] Key pressed: {:?} (ctrl={}, shift={})",
key, ctrl, shift
);
match key {
Key::Escape => {
@@ -906,10 +928,11 @@ impl MainWindow {
if let Some(selected) = results_list.selected_row() {
let prev_index = selected.index() - 1;
if prev_index >= 0
&& let Some(prev_row) = results_list.row_at_index(prev_index) {
results_list.select_row(Some(&prev_row));
Self::scroll_to_row(&scrolled, &results_list, &prev_row);
}
&& let Some(prev_row) = results_list.row_at_index(prev_index)
{
results_list.select_row(Some(&prev_row));
Self::scroll_to_row(&scrolled, &results_list, &prev_row);
}
}
gtk4::glib::Propagation::Stop
}
@@ -941,8 +964,17 @@ impl MainWindow {
gtk4::glib::Propagation::Stop
}
// Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu)
Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 |
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
Key::_1
| Key::_2
| Key::_3
| Key::_4
| Key::_5
| Key::_6
| Key::_7
| Key::_8
| Key::_9
if ctrl =>
{
info!("[UI] Ctrl+number detected: {:?}", key);
if !submenu_state.borrow().active {
let idx = match key {
@@ -968,7 +1000,11 @@ impl MainWindow {
&mode_label,
);
} else {
info!("[UI] No provider at index {}, tab_order len={}", idx, tab_order.len());
info!(
"[UI] No provider at index {}, tab_order len={}",
idx,
tab_order.len()
);
}
}
gtk4::glib::Propagation::Stop
@@ -1029,7 +1065,8 @@ impl MainWindow {
let results = current_results.borrow();
if let Some(item) = results.get(index).cloned() {
drop(results);
let should_close = Self::handle_item_action(&item, &config.borrow(), &backend);
let should_close =
Self::handle_item_action(&item, &config.borrow(), &backend);
if should_close {
window.close();
} else {
@@ -1076,7 +1113,11 @@ impl MainWindow {
}
} else if current.len() == 1 {
let idx = tab_order.iter().position(|p| p == &current[0]).unwrap_or(0);
let at_boundary = if forward { idx == tab_order.len() - 1 } else { idx == 0 };
let at_boundary = if forward {
idx == tab_order.len() - 1
} else {
idx == 0
};
if at_boundary {
// At boundary, go back to "All" mode
@@ -1284,11 +1325,14 @@ impl MainWindow {
info!("Launching: {} ({})", item.name, item.command);
#[cfg(feature = "dev-logging")]
debug!("[UI] Launch details: terminal={}, provider={:?}, id={}", item.terminal, item.provider, item.id);
debug!(
"[UI] Launch details: terminal={}, provider={:?}, id={}",
item.terminal, item.provider, item.id
);
// Check if this is a desktop application (has .desktop file as ID)
let is_desktop_app = matches!(item.provider, ProviderType::Application)
&& item.id.ends_with(".desktop");
let is_desktop_app =
matches!(item.provider, ProviderType::Application) && item.id.ends_with(".desktop");
// Desktop files should be launched via proper launchers that implement the
// freedesktop Desktop Entry spec (D-Bus activation, field codes, env vars, etc.)
@@ -1315,7 +1359,10 @@ impl MainWindow {
///
/// Otherwise, uses `gio launch` which is always available (part of glib2/GTK4)
/// and handles D-Bus activation, field codes, Terminal flag, etc.
fn launch_desktop_file(desktop_path: &str, config: &Config) -> std::io::Result<std::process::Child> {
fn launch_desktop_file(
desktop_path: &str,
config: &Config,
) -> std::io::Result<std::process::Child> {
use std::path::Path;
// Check if desktop file exists
@@ -1349,16 +1396,22 @@ impl MainWindow {
.spawn()
} else {
info!("Launching via gio: {}", desktop_path);
Command::new("gio")
.args(["launch", desktop_path])
.spawn()
Command::new("gio").args(["launch", desktop_path]).spawn()
}
}
/// Launch a shell command (for non-desktop items like PATH commands, plugins, etc.)
fn launch_command(command: &str, terminal: bool, config: &Config) -> std::io::Result<std::process::Child> {
fn launch_command(
command: &str,
terminal: bool,
config: &Config,
) -> std::io::Result<std::process::Child> {
let cmd = if terminal {
let terminal_cmd = config.general.terminal_command.as_deref().unwrap_or("xterm");
let terminal_cmd = config
.general
.terminal_command
.as_deref()
.unwrap_or("xterm");
format!("{} -e {}", terminal_cmd, command)
} else {
command.to_string()

View File

@@ -1,6 +1,6 @@
use owlry_core::providers::LaunchItem;
use gtk4::prelude::*;
use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget};
use owlry_core::providers::LaunchItem;
#[allow(dead_code)]
pub struct ResultRow {
@@ -81,7 +81,9 @@ impl ResultRow {
} else {
// Default icon based on provider type (only core types, plugins should provide icons)
let default_icon = match &item.provider {
owlry_core::providers::ProviderType::Application => "application-x-executable-symbolic",
owlry_core::providers::ProviderType::Application => {
"application-x-executable-symbolic"
}
owlry_core::providers::ProviderType::Command => "utilities-terminal-symbolic",
owlry_core::providers::ProviderType::Dmenu => "view-list-symbolic",
// Plugins should provide their own icon; fallback to generic addon icon
@@ -134,9 +136,7 @@ impl ResultRow {
.build();
for tag in item.tags.iter().take(3) {
let tag_label = Label::builder()
.label(tag)
.build();
let tag_label = Label::builder().label(tag).build();
tag_label.add_css_class("owlry-tag-badge");
tags_box.append(&tag_label);
}