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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,13 +26,16 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let client = reqwest::blocking::Client::builder() let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(timeout_secs)) .timeout(Duration::from_secs(timeout_secs))
.build() .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); let mut request = client.get(&url);
// Add custom headers if provided // Add custom headers if provided
if let Some(ref opts) = opts if let Some(ref opts) = opts
&& let Ok(headers) = opts.get::<Table>("headers") { && let Ok(headers) = opts.get::<Table>("headers")
{
for pair in headers.pairs::<String, String>() { for pair in headers.pairs::<String, String>() {
let (key, value) = pair?; let (key, value) = pair?;
request = request.header(&key, &value); request = request.header(&key, &value);
@@ -45,9 +48,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let status = response.status().as_u16(); let status = response.status().as_u16();
let headers = extract_headers(&response); let headers = extract_headers(&response);
let body = response let body = response.text().map_err(|e| {
.text() mlua::Error::external(format!("Failed to read response body: {}", e))
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?; })?;
let result = lua.create_table()?; let result = lua.create_table()?;
result.set("status", status)?; result.set("status", status)?;
@@ -78,13 +81,16 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let client = reqwest::blocking::Client::builder() let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(timeout_secs)) .timeout(Duration::from_secs(timeout_secs))
.build() .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); let mut request = client.post(&url);
// Add custom headers if provided // Add custom headers if provided
if let Some(ref opts) = opts if let Some(ref opts) = opts
&& let Ok(headers) = opts.get::<Table>("headers") { && let Ok(headers) = opts.get::<Table>("headers")
{
for pair in headers.pairs::<String, String>() { for pair in headers.pairs::<String, String>() {
let (key, value) = pair?; let (key, value) = pair?;
request = request.header(&key, &value); request = request.header(&key, &value);
@@ -102,11 +108,7 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
.body(json_str) .body(json_str)
} }
Value::Nil => request, 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 let response = request
@@ -115,9 +117,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let status = response.status().as_u16(); let status = response.status().as_u16();
let headers = extract_headers(&response); let headers = extract_headers(&response);
let body = response let body = response.text().map_err(|e| {
.text() mlua::Error::external(format!("Failed to read response body: {}", e))
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?; })?;
let result = lua.create_table()?; let result = lua.create_table()?;
result.set("status", status)?; result.set("status", status)?;
@@ -149,14 +151,17 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let client = reqwest::blocking::Client::builder() let client = reqwest::blocking::Client::builder()
.timeout(Duration::from_secs(timeout_secs)) .timeout(Duration::from_secs(timeout_secs))
.build() .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); let mut request = client.get(&url);
request = request.header("Accept", "application/json"); request = request.header("Accept", "application/json");
// Add custom headers if provided // Add custom headers if provided
if let Some(ref opts) = opts if let Some(ref opts) = opts
&& let Ok(headers) = opts.get::<Table>("headers") { && let Ok(headers) = opts.get::<Table>("headers")
{
for pair in headers.pairs::<String, String>() { for pair in headers.pairs::<String, String>() {
let (key, value) = pair?; let (key, value) = pair?;
request = request.header(&key, &value); request = request.header(&key, &value);
@@ -174,9 +179,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
))); )));
} }
let body = response let body = response.text().map_err(|e| {
.text() mlua::Error::external(format!("Failed to read response body: {}", e))
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?; })?;
// Parse JSON and convert to Lua table // Parse JSON and convert to Lua table
let json_value: serde_json::Value = serde_json::from_str(&body) let json_value: serde_json::Value = serde_json::from_str(&body)

View File

@@ -14,7 +14,8 @@ pub fn register_math_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
// Returns (result, nil) on success or (nil, error_message) on failure // Returns (result, nil) on success or (nil, error_message) on failure
math_table.set( math_table.set(
"calculate", "calculate",
lua.create_function(|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> { lua.create_function(
|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> {
match meval::eval_str(&expr) { match meval::eval_str(&expr) {
Ok(result) => { Ok(result) => {
if result.is_finite() { if result.is_finite() {
@@ -23,11 +24,10 @@ pub fn register_math_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
Ok((None, Some("Result is not a finite number".to_string()))) Ok((None, Some("Result is not a finite number".to_string())))
} }
} }
Err(e) => { Err(e) => Ok((None, Some(e.to_string()))),
Ok((None, Some(e.to_string())))
} }
} },
})?, )?,
)?; )?;
// owlry.math.calc(expression) -> number (throws on error) // owlry.math.calc(expression) -> number (throws on error)
@@ -106,11 +106,13 @@ mod tests {
fn test_calculate_basic() { fn test_calculate_basic() {
let lua = setup_lua(); let lua = setup_lua();
let chunk = lua.load(r#" let chunk = lua.load(
r#"
local result, err = owlry.math.calculate("2 + 2") local result, err = owlry.math.calculate("2 + 2")
if err then error(err) end if err then error(err) end
return result return result
"#); "#,
);
let result: f64 = chunk.call(()).unwrap(); let result: f64 = chunk.call(()).unwrap();
assert!((result - 4.0).abs() < f64::EPSILON); assert!((result - 4.0).abs() < f64::EPSILON);
} }
@@ -119,11 +121,13 @@ mod tests {
fn test_calculate_complex() { fn test_calculate_complex() {
let lua = setup_lua(); let lua = setup_lua();
let chunk = lua.load(r#" let chunk = lua.load(
r#"
local result, err = owlry.math.calculate("sqrt(16) + 2^3") local result, err = owlry.math.calculate("sqrt(16) + 2^3")
if err then error(err) end if err then error(err) end
return result return result
"#); "#,
);
let result: f64 = chunk.call(()).unwrap(); let result: f64 = chunk.call(()).unwrap();
assert!((result - 12.0).abs() < f64::EPSILON); // sqrt(16) = 4, 2^3 = 8 assert!((result - 12.0).abs() < f64::EPSILON); // sqrt(16) = 4, 2^3 = 8
} }
@@ -132,14 +136,16 @@ mod tests {
fn test_calculate_error() { fn test_calculate_error() {
let lua = setup_lua(); let lua = setup_lua();
let chunk = lua.load(r#" let chunk = lua.load(
r#"
local result, err = owlry.math.calculate("invalid expression @@") local result, err = owlry.math.calculate("invalid expression @@")
if result then if result then
return false -- should not succeed return false -- should not succeed
else else
return true -- correctly failed return true -- correctly failed
end end
"#); "#,
);
let had_error: bool = chunk.call(()).unwrap(); let had_error: bool = chunk.call(()).unwrap();
assert!(had_error); 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)))?; .map_err(|e| mlua::Error::external(format!("Failed to run command: {}", e)))?;
let result = lua.create_table()?; let result = lua.create_table()?;
result.set("stdout", String::from_utf8_lossy(&output.stdout).to_string())?; result.set(
result.set("stderr", String::from_utf8_lossy(&output.stderr).to_string())?; "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("exit_code", output.status.code().unwrap_or(-1))?;
result.set("success", output.status.success())?; 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 // owlry.env.get(name) -> string or nil
env_table.set( env_table.set(
"get", "get",
lua.create_function(|_lua, name: String| { lua.create_function(|_lua, name: String| Ok(std::env::var(&name).ok()))?,
Ok(std::env::var(&name).ok())
})?,
)?; )?;
// owlry.env.get_or(name, default) -> string // owlry.env.get_or(name, default) -> string
@@ -166,7 +170,8 @@ mod tests {
assert!(exists); assert!(exists);
// Made-up command should not exist // 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(); let not_exists: bool = chunk.call(()).unwrap();
assert!(!not_exists); assert!(!not_exists);
} }
@@ -190,7 +195,8 @@ mod tests {
fn test_env_get_or() { fn test_env_get_or() {
let lua = setup_lua(); 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(); let result: String = chunk.call(()).unwrap();
assert_eq!(result, "default_value"); assert_eq!(result, "default_value");
} }

View File

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

View File

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

View File

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

View File

@@ -112,11 +112,16 @@ pub struct PluginPermissions {
/// Discover all plugins in a directory /// Discover all plugins in a directory
/// ///
/// Returns a map of plugin ID -> (manifest, path) /// 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(); let mut plugins = HashMap::new();
if !plugins_dir.exists() { 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); 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()); log::warn!("Duplicate plugin ID '{}', skipping {}", id, path.display());
continue; 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)); plugins.insert(id, (manifest, path));
} }
Err(e) => { 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 { return Err(PluginError::InvalidManifest {
plugin: self.plugin.id.clone(), plugin: self.plugin.id.clone(),
message: "Plugin ID must be lowercase alphanumeric with hyphens".to_string(), 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() { if semver::VersionReq::parse(&self.plugin.owlry_version).is_err() {
return Err(PluginError::InvalidManifest { return Err(PluginError::InvalidManifest {
plugin: self.plugin.id.clone(), 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 // Used by plugins/commands.rs for plugin CLI commands
#[allow(unused_imports)] #[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) // Lua Plugin Manager (only available with lua feature)
@@ -64,7 +64,7 @@ mod lua_manager {
use std::path::PathBuf; use std::path::PathBuf;
use std::rc::Rc; 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 /// Plugin manager coordinates loading, initialization, and lifecycle of Lua plugins
pub struct PluginManager { pub struct PluginManager {
@@ -158,7 +158,10 @@ mod lua_manager {
/// Get all enabled plugins /// Get all enabled plugins
pub fn enabled_plugins(&self) -> impl Iterator<Item = Rc<RefCell<LoadedPlugin>>> + '_ { 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 /// Get the number of loaded plugins
@@ -176,7 +179,10 @@ mod lua_manager {
/// Enable a plugin by ID /// Enable a plugin by ID
#[allow(dead_code)] #[allow(dead_code)]
pub fn enable(&mut self, id: &str) -> PluginResult<()> { 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(); let mut plugin = plugin_rc.borrow_mut();
if !plugin.enabled { if !plugin.enabled {
@@ -191,7 +197,10 @@ mod lua_manager {
/// Disable a plugin by ID /// Disable a plugin by ID
#[allow(dead_code)] #[allow(dead_code)]
pub fn disable(&mut self, id: &str) -> PluginResult<()> { 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; plugin_rc.borrow_mut().enabled = false;
Ok(()) Ok(())
} }
@@ -200,7 +209,13 @@ mod lua_manager {
#[allow(dead_code)] #[allow(dead_code)]
pub fn providers_for(&self, provider_name: &str) -> Vec<String> { pub fn providers_for(&self, provider_name: &str) -> Vec<String> {
self.enabled_plugins() 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()) .map(|p| p.borrow().id().to_string())
.collect() .collect()
} }
@@ -208,13 +223,15 @@ mod lua_manager {
/// Check if any plugin provides actions /// Check if any plugin provides actions
#[allow(dead_code)] #[allow(dead_code)]
pub fn has_action_plugins(&self) -> bool { 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 /// Check if any plugin provides hooks
#[allow(dead_code)] #[allow(dead_code)]
pub fn has_hook_plugins(&self) -> bool { 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 /// Get all theme names provided by plugins

View File

@@ -17,8 +17,8 @@ use std::sync::{Arc, Once};
use libloading::Library; use libloading::Library;
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
use owlry_plugin_api::{ use owlry_plugin_api::{
HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION, HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo,
RStr, API_VERSION, ProviderKind, RStr,
}; };
use crate::notify; use crate::notify;
@@ -28,9 +28,18 @@ use crate::notify;
// ============================================================================ // ============================================================================
/// Host notification handler /// 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_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 { let notify_urgency = match urgency {
NotifyUrgency::Low => notify::NotifyUrgency::Low, NotifyUrgency::Low => notify::NotifyUrgency::Low,
@@ -121,7 +130,9 @@ impl NativePlugin {
handle: ProviderHandle, handle: ProviderHandle,
query: &str, query: &str,
) -> Vec<owlry_plugin_api::PluginItem> { ) -> 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 /// Drop a provider handle

View File

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

View File

@@ -49,11 +49,7 @@ pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> {
// Create Lua with safe standard libraries only // Create Lua with safe standard libraries only
// ALL_SAFE excludes: debug, io, os (dangerous parts), package (loadlib), ffi // ALL_SAFE excludes: debug, io, os (dangerous parts), package (loadlib), ffi
// We then customize the os table to only allow safe functions // We then customize the os table to only allow safe functions
let libs = StdLib::COROUTINE let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
| StdLib::TABLE
| StdLib::STRING
| StdLib::UTF8
| StdLib::MATH;
let lua = Lua::new_with(libs, mlua::LuaOptions::default())?; 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 // We do NOT include: os.exit, os.remove, os.rename, os.setlocale, os.tmpname
// and the shell-related functions // and the shell-related functions
let os_table = lua.create_table()?; 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("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)?)?; os_table.set("time", lua.create_function(os_time)?)?;
globals.set("os", os_table)?; 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 /// Load and run a Lua file in the given runtime
pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> { pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> {
let content = std::fs::read_to_string(path) let content = std::fs::read_to_string(path).map_err(mlua::Error::external)?;
.map_err(mlua::Error::external)?;
lua.load(&content) lua.load(&content)
.set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk")) .set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk"))
.into_function()? .into_function()?

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,9 @@
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};
use log::debug; 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 super::{LaunchItem, Provider, ProviderType};
use crate::plugins::native_loader::NativePlugin; use crate::plugins::native_loader::NativePlugin;
@@ -76,7 +78,10 @@ impl NativeProvider {
} }
let api_items = self.plugin.query_provider(self.handle, query); 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 /// 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 pm_guard = pm.lock().unwrap();
let frecency_guard = frecency.lock().unwrap(); let frecency_guard = frecency.lock().unwrap();
let results = let results = pm_guard.search_with_frecency(
pm_guard.search_with_frecency(text, max, &filter, &frecency_guard, weight, None); text,
max,
&filter,
&frecency_guard,
weight,
None,
);
Response::Results { Response::Results {
items: 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(); let mut frecency_guard = frecency.lock().unwrap();
frecency_guard.record_launch(item_id); frecency_guard.record_launch(item_id);
Response::Ack Response::Ack

View File

@@ -122,7 +122,8 @@ fn test_plugin_action_request() {
#[test] #[test]
fn test_terminal_field_defaults_false() { fn test_terminal_field_defaults_false() {
// terminal field should default to false when missing from JSON // 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(); let item: ResultItem = serde_json::from_str(json).unwrap();
assert!(!item.terminal); assert!(!item.terminal);
} }

View File

@@ -37,7 +37,11 @@ fn test_server_responds_to_providers_request() {
match resp { match resp {
Response::Providers { list } => { Response::Providers { list } => {
// The default ProviderManager always has at least Application and Command // 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(); let ids: Vec<&str> = list.iter().map(|p| p.id.as_str()).collect();
assert!(ids.contains(&"app"), "missing 'app' provider"); assert!(ids.contains(&"app"), "missing 'app' provider");
assert!(ids.contains(&"cmd"), "missing 'cmd' provider"); assert!(ids.contains(&"cmd"), "missing 'cmd' provider");
@@ -95,7 +99,10 @@ fn test_server_handles_query_request() {
Response::Results { items } => { Response::Results { items } => {
// A nonsense query should return empty or very few results // A nonsense query should return empty or very few results
// (no items will fuzzy-match "nonexistent_query_xyz") // (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), 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" "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); drop(stream);

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-lua" name = "owlry-lua"
version = "0.4.10" version = "1.0.0"
edition.workspace = true edition.workspace = true
rust-version.workspace = true rust-version.workspace = true
license.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() /// Implementation of owlry.provider.register()
fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> { fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> {
let name: String = config.get("name")?; 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()); .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('-', "_")); .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()); .unwrap_or_else(|| "application-x-addon".to_string());
let prefix: Option<String> = config.get("prefix")?; let prefix: Option<String> = config.get("prefix")?;
@@ -116,7 +119,8 @@ fn call_provider_function(
// First check if there's a _providers table // First check if there's a _providers table
if let Ok(Value::Table(providers)) = globals.get::<Value>("_owlry_providers") if let Ok(Value::Table(providers)) = globals.get::<Value>("_owlry_providers")
&& let Ok(Value::Table(config)) = providers.get::<Value>(provider_name) && let Ok(Value::Table(config)) = providers.get::<Value>(provider_name)
&& let Ok(Value::Function(func)) = config.get::<Value>(function_name) { && let Ok(Value::Function(func)) = config.get::<Value>(function_name)
{
let result: Value = match query { let result: Value = match query {
Some(q) => func.call(q)?, Some(q) => func.call(q)?,
None => func.call(())?, None => func.call(())?,
@@ -153,7 +157,9 @@ fn parse_item(table: &Table) -> LuaResult<PluginItem> {
let description: Option<String> = table.get("description")?; let description: Option<String> = table.get("description")?;
let icon: Option<String> = table.get("icon")?; let icon: Option<String> = table.get("icon")?;
let terminal: bool = table.get::<Option<bool>>("terminal")?.unwrap_or(false); 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); let mut item = PluginItem::new(id, name, command);
@@ -176,7 +182,7 @@ fn parse_item(table: &Table) -> LuaResult<PluginItem> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::runtime::{create_lua_runtime, SandboxConfig}; use crate::runtime::{SandboxConfig, create_lua_runtime};
#[test] #[test]
fn test_register_static_provider() { 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<()> { pub fn register_log_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let log = lua.create_table()?; let log = lua.create_table()?;
log.set("debug", lua.create_function(|_, msg: String| { log.set(
"debug",
lua.create_function(|_, msg: String| {
eprintln!("[DEBUG] {}", msg); eprintln!("[DEBUG] {}", msg);
Ok(()) Ok(())
})?)?; })?,
)?;
log.set("info", lua.create_function(|_, msg: String| { log.set(
"info",
lua.create_function(|_, msg: String| {
eprintln!("[INFO] {}", msg); eprintln!("[INFO] {}", msg);
Ok(()) Ok(())
})?)?; })?,
)?;
log.set("warn", lua.create_function(|_, msg: String| { log.set(
"warn",
lua.create_function(|_, msg: String| {
eprintln!("[WARN] {}", msg); eprintln!("[WARN] {}", msg);
Ok(()) Ok(())
})?)?; })?,
)?;
log.set("error", lua.create_function(|_, msg: String| { log.set(
"error",
lua.create_function(|_, msg: String| {
eprintln!("[ERROR] {}", msg); eprintln!("[ERROR] {}", msg);
Ok(()) Ok(())
})?)?; })?,
)?;
owlry.set("log", log)?; owlry.set("log", log)?;
Ok(()) Ok(())
@@ -44,59 +56,79 @@ pub fn register_path_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResu
let path = lua.create_table()?; let path = lua.create_table()?;
// owlry.path.config() -> ~/.config/owlry // owlry.path.config() -> ~/.config/owlry
path.set("config", lua.create_function(|_, ()| { path.set(
"config",
lua.create_function(|_, ()| {
Ok(dirs::config_dir() Ok(dirs::config_dir()
.map(|d| d.join("owlry")) .map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()) .unwrap_or_default())
})?)?; })?,
)?;
// owlry.path.data() -> ~/.local/share/owlry // owlry.path.data() -> ~/.local/share/owlry
path.set("data", lua.create_function(|_, ()| { path.set(
"data",
lua.create_function(|_, ()| {
Ok(dirs::data_dir() Ok(dirs::data_dir()
.map(|d| d.join("owlry")) .map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()) .unwrap_or_default())
})?)?; })?,
)?;
// owlry.path.cache() -> ~/.cache/owlry // owlry.path.cache() -> ~/.cache/owlry
path.set("cache", lua.create_function(|_, ()| { path.set(
"cache",
lua.create_function(|_, ()| {
Ok(dirs::cache_dir() Ok(dirs::cache_dir()
.map(|d| d.join("owlry")) .map(|d| d.join("owlry"))
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()) .unwrap_or_default())
})?)?; })?,
)?;
// owlry.path.home() -> ~ // owlry.path.home() -> ~
path.set("home", lua.create_function(|_, ()| { path.set(
"home",
lua.create_function(|_, ()| {
Ok(dirs::home_dir() Ok(dirs::home_dir()
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_default()) .unwrap_or_default())
})?)?; })?,
)?;
// owlry.path.join(...) -> joined path // owlry.path.join(...) -> joined path
path.set("join", lua.create_function(|_, parts: mlua::Variadic<String>| { path.set(
"join",
lua.create_function(|_, parts: mlua::Variadic<String>| {
let mut path = PathBuf::new(); let mut path = PathBuf::new();
for part in parts { for part in parts {
path.push(part); path.push(part);
} }
Ok(path.to_string_lossy().to_string()) Ok(path.to_string_lossy().to_string())
})?)?; })?,
)?;
// owlry.path.plugin_dir() -> plugin directory // owlry.path.plugin_dir() -> plugin directory
let plugin_dir_str = plugin_dir.to_string_lossy().to_string(); let plugin_dir_str = plugin_dir.to_string_lossy().to_string();
path.set("plugin_dir", lua.create_function(move |_, ()| { path.set(
Ok(plugin_dir_str.clone()) "plugin_dir",
})?)?; lua.create_function(move |_, ()| Ok(plugin_dir_str.clone()))?,
)?;
// owlry.path.expand(path) -> expanded path (~ -> home) // owlry.path.expand(path) -> expanded path (~ -> home)
path.set("expand", lua.create_function(|_, path: String| { path.set(
"expand",
lua.create_function(|_, path: String| {
if path.starts_with("~/") if path.starts_with("~/")
&& let Some(home) = dirs::home_dir() { && let Some(home) = dirs::home_dir()
{
return Ok(home.join(&path[2..]).to_string_lossy().to_string()); return Ok(home.join(&path[2..]).to_string_lossy().to_string());
} }
Ok(path) Ok(path)
})?)?; })?,
)?;
owlry.set("path", path)?; owlry.set("path", path)?;
Ok(()) Ok(())
@@ -111,28 +143,39 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, _plugin_dir: &Path) -> LuaResul
let fs = lua.create_table()?; let fs = lua.create_table()?;
// owlry.fs.exists(path) -> bool // owlry.fs.exists(path) -> bool
fs.set("exists", lua.create_function(|_, path: String| { fs.set(
"exists",
lua.create_function(|_, path: String| {
let path = expand_path(&path); let path = expand_path(&path);
Ok(Path::new(&path).exists()) Ok(Path::new(&path).exists())
})?)?; })?,
)?;
// owlry.fs.is_dir(path) -> bool // owlry.fs.is_dir(path) -> bool
fs.set("is_dir", lua.create_function(|_, path: String| { fs.set(
"is_dir",
lua.create_function(|_, path: String| {
let path = expand_path(&path); let path = expand_path(&path);
Ok(Path::new(&path).is_dir()) Ok(Path::new(&path).is_dir())
})?)?; })?,
)?;
// owlry.fs.read(path) -> string or nil // owlry.fs.read(path) -> string or nil
fs.set("read", lua.create_function(|_, path: String| { fs.set(
"read",
lua.create_function(|_, path: String| {
let path = expand_path(&path); let path = expand_path(&path);
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&path) {
Ok(content) => Ok(Some(content)), Ok(content) => Ok(Some(content)),
Err(_) => Ok(None), Err(_) => Ok(None),
} }
})?)?; })?,
)?;
// owlry.fs.read_lines(path) -> table of strings or nil // owlry.fs.read_lines(path) -> table of strings or nil
fs.set("read_lines", lua.create_function(|lua, path: String| { fs.set(
"read_lines",
lua.create_function(|lua, path: String| {
let path = expand_path(&path); let path = expand_path(&path);
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&path) {
Ok(content) => { Ok(content) => {
@@ -141,10 +184,13 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, _plugin_dir: &Path) -> LuaResul
} }
Err(_) => Ok(None), Err(_) => Ok(None),
} }
})?)?; })?,
)?;
// owlry.fs.list_dir(path) -> table of filenames or nil // owlry.fs.list_dir(path) -> table of filenames or nil
fs.set("list_dir", lua.create_function(|lua, path: String| { fs.set(
"list_dir",
lua.create_function(|lua, path: String| {
let path = expand_path(&path); let path = expand_path(&path);
match std::fs::read_dir(&path) { match std::fs::read_dir(&path) {
Ok(entries) => { Ok(entries) => {
@@ -156,31 +202,36 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, _plugin_dir: &Path) -> LuaResul
} }
Err(_) => Ok(None), Err(_) => Ok(None),
} }
})?)?; })?,
)?;
// owlry.fs.read_json(path) -> table or nil // owlry.fs.read_json(path) -> table or nil
fs.set("read_json", lua.create_function(|lua, path: String| { fs.set(
"read_json",
lua.create_function(|lua, path: String| {
let path = expand_path(&path); let path = expand_path(&path);
match std::fs::read_to_string(&path) { match std::fs::read_to_string(&path) {
Ok(content) => { Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
match serde_json::from_str::<serde_json::Value>(&content) {
Ok(value) => json_to_lua(lua, &value), Ok(value) => json_to_lua(lua, &value),
Err(_) => Ok(Value::Nil), Err(_) => Ok(Value::Nil),
} },
}
Err(_) => Ok(Value::Nil), Err(_) => Ok(Value::Nil),
} }
})?)?; })?,
)?;
// owlry.fs.write(path, content) -> bool // owlry.fs.write(path, content) -> bool
fs.set("write", lua.create_function(|_, (path, content): (String, String)| { fs.set(
"write",
lua.create_function(|_, (path, content): (String, String)| {
let path = expand_path(&path); let path = expand_path(&path);
// Create parent directories if needed // Create parent directories if needed
if let Some(parent) = Path::new(&path).parent() { if let Some(parent) = Path::new(&path).parent() {
let _ = std::fs::create_dir_all(parent); let _ = std::fs::create_dir_all(parent);
} }
Ok(std::fs::write(&path, content).is_ok()) Ok(std::fs::write(&path, content).is_ok())
})?)?; })?,
)?;
owlry.set("fs", fs)?; owlry.set("fs", fs)?;
Ok(()) Ok(())
@@ -195,18 +246,24 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
let json = lua.create_table()?; let json = lua.create_table()?;
// owlry.json.encode(value) -> string // owlry.json.encode(value) -> string
json.set("encode", lua.create_function(|lua, value: Value| { json.set(
"encode",
lua.create_function(|lua, value: Value| {
let json_value = lua_to_json(lua, &value)?; let json_value = lua_to_json(lua, &value)?;
Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string())) Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string()))
})?)?; })?,
)?;
// owlry.json.decode(string) -> value or nil // owlry.json.decode(string) -> value or nil
json.set("decode", lua.create_function(|lua, s: String| { json.set(
"decode",
lua.create_function(|lua, s: String| {
match serde_json::from_str::<serde_json::Value>(&s) { match serde_json::from_str::<serde_json::Value>(&s) {
Ok(value) => json_to_lua(lua, &value), Ok(value) => json_to_lua(lua, &value),
Err(_) => Ok(Value::Nil), Err(_) => Ok(Value::Nil),
} }
})?)?; })?,
)?;
owlry.set("json", json)?; owlry.set("json", json)?;
Ok(()) Ok(())
@@ -219,7 +276,8 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
/// Expand ~ in paths /// Expand ~ in paths
fn expand_path(path: &str) -> String { fn expand_path(path: &str) -> String {
if path.starts_with("~/") if path.starts_with("~/")
&& let Some(home) = dirs::home_dir() { && let Some(home) = dirs::home_dir()
{
return home.join(&path[2..]).to_string_lossy().to_string(); return home.join(&path[2..]).to_string_lossy().to_string();
} }
path.to_string() path.to_string()
@@ -305,7 +363,7 @@ fn lua_to_json(_lua: &Lua, value: &Value) -> LuaResult<serde_json::Value> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::runtime::{create_lua_runtime, SandboxConfig}; use crate::runtime::{SandboxConfig, create_lua_runtime};
#[test] #[test]
fn test_log_api() { fn test_log_api() {
@@ -316,7 +374,10 @@ mod tests {
lua.globals().set("owlry", owlry).unwrap(); lua.globals().set("owlry", owlry).unwrap();
// Just verify it doesn't panic // 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] #[test]
@@ -327,10 +388,18 @@ mod tests {
register_path_api(&lua, &owlry, Path::new("/tmp/test-plugin")).unwrap(); register_path_api(&lua, &owlry, Path::new("/tmp/test-plugin")).unwrap();
lua.globals().set("owlry", owlry).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()); 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"); assert_eq!(plugin_dir, "/tmp/test-plugin");
} }
@@ -342,10 +411,18 @@ mod tests {
register_fs_api(&lua, &owlry, Path::new("/tmp")).unwrap(); register_fs_api(&lua, &owlry, Path::new("/tmp")).unwrap();
lua.globals().set("owlry", owlry).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); 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); assert!(is_dir);
} }

View File

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

View File

@@ -8,7 +8,7 @@ use owlry_plugin_api::PluginItem;
use crate::api; use crate::api;
use crate::manifest::PluginManifest; 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 /// Provider registration info from Lua
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -77,11 +77,13 @@ impl LoadedPlugin {
// Load the entry point file // Load the entry point file
let entry_path = self.path.join(&self.manifest.plugin.entry); let entry_path = self.path.join(&self.manifest.plugin.entry);
if !entry_path.exists() { 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) load_file(&lua, &entry_path).map_err(|e| format!("Failed to load entry point: {}", e))?;
.map_err(|e| format!("Failed to load entry point: {}", e))?;
self.lua = Some(lua); self.lua = Some(lua);
Ok(()) Ok(())
@@ -89,7 +91,9 @@ impl LoadedPlugin {
/// Get provider registrations from this plugin /// Get provider registrations from this plugin
pub fn get_provider_registrations(&self) -> Result<Vec<ProviderRegistration>, String> { 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())?; .ok_or_else(|| "Plugin not initialized".to_string())?;
api::get_provider_registrations(lua) api::get_provider_registrations(lua)
@@ -98,25 +102,33 @@ impl LoadedPlugin {
/// Call a provider's refresh function /// Call a provider's refresh function
pub fn call_provider_refresh(&self, provider_name: &str) -> Result<Vec<PluginItem>, String> { 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())?; .ok_or_else(|| "Plugin not initialized".to_string())?;
api::call_refresh(lua, provider_name) api::call_refresh(lua, provider_name).map_err(|e| format!("Refresh failed: {}", e))
.map_err(|e| format!("Refresh failed: {}", e))
} }
/// Call a provider's query function /// Call a provider's query function
pub fn call_provider_query(&self, provider_name: &str, query: &str) -> Result<Vec<PluginItem>, String> { pub fn call_provider_query(
let lua = self.lua.as_ref() &self,
provider_name: &str,
query: &str,
) -> Result<Vec<PluginItem>, String> {
let lua = self
.lua
.as_ref()
.ok_or_else(|| "Plugin not initialized".to_string())?; .ok_or_else(|| "Plugin not initialized".to_string())?;
api::call_query(lua, provider_name, query) api::call_query(lua, provider_name, query).map_err(|e| format!("Query failed: {}", e))
.map_err(|e| format!("Query failed: {}", e))
} }
} }
/// Discover plugins in a directory /// 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(); let mut plugins = HashMap::new();
if !plugins_dir.exists() { if !plugins_dir.exists() {
@@ -146,13 +158,21 @@ pub fn discover_plugins(plugins_dir: &Path) -> Result<HashMap<String, (PluginMan
Ok(manifest) => { Ok(manifest) => {
let id = manifest.plugin.id.clone(); let id = manifest.plugin.id.clone();
if plugins.contains_key(&id) { 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; continue;
} }
plugins.insert(id, (manifest, path)); plugins.insert(id, (manifest, path));
} }
Err(e) => { 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 { impl PluginManifest {
/// Load a plugin manifest from a plugin.toml file /// Load a plugin manifest from a plugin.toml file
pub fn load(path: &Path) -> Result<Self, String> { pub fn load(path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(path) let content =
.map_err(|e| format!("Failed to read manifest: {}", e))?; std::fs::read_to_string(path).map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: PluginManifest = toml::from_str(&content) let manifest: PluginManifest =
.map_err(|e| format!("Failed to parse manifest: {}", e))?; toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
manifest.validate()?; manifest.validate()?;
Ok(manifest) Ok(manifest)
} }
@@ -105,7 +105,12 @@ impl PluginManifest {
return Err("Plugin ID cannot be empty".to_string()); 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()); return Err("Plugin ID must be lowercase alphanumeric with hyphens".to_string());
} }
@@ -116,7 +121,10 @@ impl PluginManifest {
// Validate owlry_version constraint // Validate owlry_version constraint
if semver::VersionReq::parse(&self.plugin.owlry_version).is_err() { 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(()) Ok(())

View File

@@ -50,11 +50,7 @@ impl SandboxConfig {
pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> { pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> {
// Create Lua with safe standard libraries only // Create Lua with safe standard libraries only
// We exclude: debug, io, os (dangerous parts), package (loadlib), ffi // We exclude: debug, io, os (dangerous parts), package (loadlib), ffi
let libs = StdLib::COROUTINE let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
| StdLib::TABLE
| StdLib::STRING
| StdLib::UTF8
| StdLib::MATH;
let lua = Lua::new_with(libs, mlua::LuaOptions::default())?; 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 // Create a restricted os table with only safe functions
let os_table = lua.create_table()?; let os_table = lua.create_table()?;
os_table.set("clock", lua.create_function(|_, ()| { os_table.set(
Ok(std::time::Instant::now().elapsed().as_secs_f64()) "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("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)?)?; os_table.set("time", lua.create_function(os_time)?)?;
globals.set("os", os_table)?; 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 /// Load and run a Lua file in the given runtime
pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> { pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> {
let content = std::fs::read_to_string(path) let content = std::fs::read_to_string(path).map_err(mlua::Error::external)?;
.map_err(mlua::Error::external)?;
lua.load(&content) lua.load(&content)
.set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk")) .set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk"))
.into_function()? .into_function()?

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry-rune" name = "owlry-rune"
version = "0.4.10" version = "1.0.0"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "Rune scripting runtime for owlry plugins" 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 init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<RuneProviderInfo>, pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<RuneProviderInfo>,
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>, 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), 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 _ = env_logger::try_init();
let plugins_dir = PathBuf::from(plugins_dir.as_str()); 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 { let mut state = RuntimeState {
plugins: HashMap::new(), 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()), type_id: RString::from(reg.type_id.as_str()),
default_icon: RString::from(reg.default_icon.as_str()), default_icon: RString::from(reg.default_icon.as_str()),
is_static: reg.is_static, is_static: reg.is_static,
prefix: reg.prefix.as_ref() prefix: reg
.prefix
.as_ref()
.map(|p| RString::from(p.as_str())) .map(|p| RString::from(p.as_str()))
.into(), .into(),
}); });
} }
state.plugins.insert(id, plugin); state.plugins.insert(id, plugin);
} }
log::info!("Loaded {} Rune plugin(s) with {} provider(s)", log::info!(
state.plugins.len(), state.providers.len()); "Loaded {} Rune plugin(s) with {} provider(s)",
state.plugins.len(),
state.providers.len()
);
} }
Err(e) => { Err(e) => {
log::error!("Failed to discover Rune plugins: {}", 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::api::{self, ProviderRegistration};
use crate::manifest::PluginManifest; 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; use owlry_plugin_api::PluginItem;
@@ -29,8 +29,8 @@ impl LoadedPlugin {
/// Create and initialize a new plugin /// Create and initialize a new plugin
pub fn new(manifest: PluginManifest, path: PathBuf) -> Result<Self, String> { pub fn new(manifest: PluginManifest, path: PathBuf) -> Result<Self, String> {
let sandbox = SandboxConfig::from_permissions(&manifest.permissions); let sandbox = SandboxConfig::from_permissions(&manifest.permissions);
let context = create_context(&sandbox) let context =
.map_err(|e| format!("Failed to create context: {}", e))?; create_context(&sandbox).map_err(|e| format!("Failed to create context: {}", e))?;
let entry_path = path.join(&manifest.plugin.entry); let entry_path = path.join(&manifest.plugin.entry);
if !entry_path.exists() { if !entry_path.exists() {
@@ -45,15 +45,14 @@ impl LoadedPlugin {
.map_err(|e| format!("Failed to compile: {}", e))?; .map_err(|e| format!("Failed to compile: {}", e))?;
// Run the entry point to register providers // Run the entry point to register providers
let mut vm = create_vm(&context, unit.clone()) let mut vm =
.map_err(|e| format!("Failed to create VM: {}", e))?; create_vm(&context, unit.clone()).map_err(|e| format!("Failed to create VM: {}", e))?;
// Execute the main function if it exists // Execute the main function if it exists
match vm.call(rune::Hash::type_hash(["main"]), ()) { match vm.call(rune::Hash::type_hash(["main"]), ()) {
Ok(result) => { Ok(result) => {
// Try to complete the execution // Try to complete the execution
let _: () = rune::from_value(result) let _: () = rune::from_value(result).unwrap_or(());
.unwrap_or(());
} }
Err(_) => { Err(_) => {
// No main function is okay // 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(); let mut plugins = HashMap::new();
if !plugins_dir.exists() { 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); 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) { let manifest = match PluginManifest::load(&manifest_path) {
Ok(m) => m, Ok(m) => m,
Err(e) => { 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; continue;
} }
}; };

View File

@@ -64,10 +64,10 @@ pub struct PluginPermissions {
impl PluginManifest { impl PluginManifest {
/// Load manifest from a plugin.toml file /// Load manifest from a plugin.toml file
pub fn load(path: &Path) -> Result<Self, String> { pub fn load(path: &Path) -> Result<Self, String> {
let content = std::fs::read_to_string(path) let content =
.map_err(|e| format!("Failed to read manifest: {}", e))?; std::fs::read_to_string(path).map_err(|e| format!("Failed to read manifest: {}", e))?;
let manifest: PluginManifest = toml::from_str(&content) let manifest: PluginManifest =
.map_err(|e| format!("Failed to parse manifest: {}", e))?; toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
manifest.validate()?; manifest.validate()?;
Ok(manifest) Ok(manifest)
} }
@@ -78,7 +78,12 @@ impl PluginManifest {
return Err("Plugin ID cannot be empty".to_string()); 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()); 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>, pub allowed_commands: Vec<String>,
} }
impl SandboxConfig { impl SandboxConfig {
/// Create sandbox config from plugin permissions /// Create sandbox config from plugin permissions
pub fn from_permissions(permissions: &PluginPermissions) -> Self { 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 /// Compile Rune source code into a Unit
pub fn compile_source( pub fn compile_source(context: &Context, source_path: &Path) -> Result<Arc<Unit>, CompileError> {
context: &Context, let source_content =
source_path: &Path, std::fs::read_to_string(source_path).map_err(|e| CompileError::Io(e.to_string()))?;
) -> 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 let source_name = source_path
.file_name() .file_name()
@@ -73,7 +69,10 @@ pub fn compile_source(
let mut sources = Sources::new(); let mut sources = Sources::new();
sources 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)))?; .map_err(|e| CompileError::Compile(format!("Failed to insert source: {}", e)))?;
let mut diagnostics = Diagnostics::new(); let mut diagnostics = Diagnostics::new();
@@ -97,13 +96,11 @@ pub fn compile_source(
} }
/// Create a new Rune VM from compiled unit /// Create a new Rune VM from compiled unit
pub fn create_vm( pub fn create_vm(context: &Context, unit: Arc<Unit>) -> Result<Vm, CompileError> {
context: &Context,
unit: Arc<Unit>,
) -> Result<Vm, CompileError> {
let runtime = Arc::new( let runtime = Arc::new(
context.runtime() context
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))? .runtime()
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))?,
); );
Ok(Vm::new(runtime, unit)) Ok(Vm::new(runtime, unit))
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "owlry" name = "owlry"
version = "0.4.10" version = "1.0.0"
edition = "2024" edition = "2024"
rust-version = "1.90" rust-version = "1.90"
description = "A lightweight, owl-themed application launcher for Wayland" 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::providers::DmenuProvider;
use crate::theme; use crate::theme;
use crate::ui::MainWindow; 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::config::Config;
use owlry_core::data::FrecencyStore; use owlry_core::data::FrecencyStore;
use owlry_core::filter::ProviderFilter; use owlry_core::filter::ProviderFilter;
use owlry_core::paths; use owlry_core::paths;
use owlry_core::providers::{Provider, ProviderManager, ProviderType}; 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::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
@@ -61,7 +61,7 @@ impl OwlryApp {
let frecency = FrecencyStore::load_or_default(); let frecency = FrecencyStore::load_or_default();
SearchBackend::Local { SearchBackend::Local {
providers: provider_manager, providers: Box::new(provider_manager),
frecency, frecency,
} }
} else { } else {
@@ -98,11 +98,7 @@ impl OwlryApp {
&config.borrow().providers, &config.borrow().providers,
) )
} else { } else {
ProviderFilter::new( ProviderFilter::new(None, Some(provider_types), &config.borrow().providers)
None,
Some(provider_types),
&config.borrow().providers,
)
} }
} else { } else {
ProviderFilter::new(None, None, &config.borrow().providers) ProviderFilter::new(None, None, &config.borrow().providers)
@@ -180,7 +176,7 @@ impl OwlryApp {
let frecency = FrecencyStore::load_or_default(); let frecency = FrecencyStore::load_or_default();
SearchBackend::Local { SearchBackend::Local {
providers: provider_manager, providers: Box::new(provider_manager),
frecency, frecency,
} }
} }
@@ -241,7 +237,8 @@ impl OwlryApp {
// 3. Load user's custom stylesheet if exists // 3. Load user's custom stylesheet if exists
if let Some(custom_path) = paths::custom_style_file() if let Some(custom_path) = paths::custom_style_file()
&& custom_path.exists() { && custom_path.exists()
{
let custom_provider = CssProvider::new(); let custom_provider = CssProvider::new();
custom_provider.load_from_path(&custom_path); custom_provider.load_from_path(&custom_path);
gtk4::style_context_add_provider_for_display( gtk4::style_context_add_provider_for_display(

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
mod app; mod app;
mod backend; mod backend;
pub mod client;
mod cli; mod cli;
pub mod client;
mod plugin_commands; mod plugin_commands;
mod providers; mod providers;
mod theme; mod theme;
@@ -65,7 +65,11 @@ fn main() {
} }
// No subcommand - launch the app // 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)) env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level))
.format_timestamp_millis() .format_timestamp_millis()

View File

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

View File

@@ -1,5 +1,5 @@
use owlry_core::providers::{LaunchItem, Provider, ProviderType};
use log::debug; use log::debug;
use owlry_core::providers::{LaunchItem, Provider, ProviderType};
use std::io::{self, BufRead}; use std::io::{self, BufRead};
/// Provider for dmenu-style input from stdin /// 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 // Always inject layout config values
css.push_str(&format!(" --owlry-font-size: {}px;\n", config.font_size)); 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 // Only inject colors if user specified them
if let Some(ref bg) = config.colors.background { 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)); css.push_str(&format!(" --owlry-text: {};\n", text));
} }
if let Some(ref text_secondary) = config.colors.text_secondary { 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 { if let Some(ref accent) = config.colors.accent {
css.push_str(&format!(" --owlry-accent: {};\n", 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)); css.push_str(&format!(" --owlry-badge-app: {};\n", badge_app));
} }
if let Some(ref badge_bookmark) = config.colors.badge_bookmark { 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 { if let Some(ref badge_calc) = config.colors.badge_calc {
css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc)); css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc));

View File

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

View File

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