chore: format, fix clippy warnings, bump all crates to 1.0.0
This commit is contained in:
10
Cargo.lock
generated
10
Cargo.lock
generated
@@ -2417,7 +2417,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"clap",
|
||||
@@ -2437,7 +2437,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-core"
|
||||
version = "0.5.0"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"ctrlc",
|
||||
@@ -2462,7 +2462,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-lua"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"chrono",
|
||||
@@ -2480,7 +2480,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-plugin-api"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"abi_stable",
|
||||
"serde",
|
||||
@@ -2488,7 +2488,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "owlry-rune"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dirs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-core"
|
||||
version = "0.5.0"
|
||||
version = "1.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -73,11 +73,7 @@ fn default_max_results() -> usize {
|
||||
}
|
||||
|
||||
fn default_tabs() -> Vec<String> {
|
||||
vec![
|
||||
"app".to_string(),
|
||||
"cmd".to_string(),
|
||||
"uuctl".to_string(),
|
||||
]
|
||||
vec!["app".to_string(), "cmd".to_string(), "uuctl".to_string()]
|
||||
}
|
||||
|
||||
/// User-customizable theme colors
|
||||
@@ -143,10 +139,18 @@ impl Default for AppearanceConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn default_width() -> i32 { 850 }
|
||||
fn default_height() -> i32 { 650 }
|
||||
fn default_font_size() -> u32 { 14 }
|
||||
fn default_border_radius() -> u32 { 12 }
|
||||
fn default_width() -> i32 {
|
||||
850
|
||||
}
|
||||
fn default_height() -> i32 {
|
||||
650
|
||||
}
|
||||
fn default_font_size() -> u32 {
|
||||
14
|
||||
}
|
||||
fn default_border_radius() -> u32 {
|
||||
12
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProvidersConfig {
|
||||
@@ -196,7 +200,6 @@ pub struct ProvidersConfig {
|
||||
pub files: bool,
|
||||
|
||||
// ─── Widget Providers ───────────────────────────────────────────────
|
||||
|
||||
/// Enable MPRIS media player widget
|
||||
#[serde(default = "default_true")]
|
||||
pub media: bool,
|
||||
@@ -350,28 +353,19 @@ impl PluginsConfig {
|
||||
/// Get a string value from a plugin's config
|
||||
#[allow(dead_code)]
|
||||
pub fn get_plugin_string(&self, plugin_name: &str, key: &str) -> Option<&str> {
|
||||
self.plugin_configs
|
||||
.get(plugin_name)?
|
||||
.get(key)?
|
||||
.as_str()
|
||||
self.plugin_configs.get(plugin_name)?.get(key)?.as_str()
|
||||
}
|
||||
|
||||
/// Get an integer value from a plugin's config
|
||||
#[allow(dead_code)]
|
||||
pub fn get_plugin_int(&self, plugin_name: &str, key: &str) -> Option<i64> {
|
||||
self.plugin_configs
|
||||
.get(plugin_name)?
|
||||
.get(key)?
|
||||
.as_integer()
|
||||
self.plugin_configs.get(plugin_name)?.get(key)?.as_integer()
|
||||
}
|
||||
|
||||
/// Get a boolean value from a plugin's config
|
||||
#[allow(dead_code)]
|
||||
pub fn get_plugin_bool(&self, plugin_name: &str, key: &str) -> Option<bool> {
|
||||
self.plugin_configs
|
||||
.get(plugin_name)?
|
||||
.get(key)?
|
||||
.as_bool()
|
||||
self.plugin_configs.get(plugin_name)?.get(key)?.as_bool()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -414,7 +408,6 @@ fn default_pomodoro_break() -> u32 {
|
||||
5
|
||||
}
|
||||
|
||||
|
||||
/// Detect the best available terminal emulator
|
||||
/// Fallback chain:
|
||||
/// 1. $TERMINAL env var (user's explicit preference)
|
||||
@@ -427,10 +420,12 @@ fn default_pomodoro_break() -> u32 {
|
||||
fn detect_terminal() -> String {
|
||||
// 1. Check $TERMINAL env var first (user's explicit preference)
|
||||
if let Ok(term) = std::env::var("TERMINAL")
|
||||
&& !term.is_empty() && command_exists(&term) {
|
||||
debug!("Using $TERMINAL: {}", term);
|
||||
return term;
|
||||
}
|
||||
&& !term.is_empty()
|
||||
&& command_exists(&term)
|
||||
{
|
||||
debug!("Using $TERMINAL: {}", term);
|
||||
return term;
|
||||
}
|
||||
|
||||
// 2. Try xdg-terminal-exec (freedesktop standard)
|
||||
if command_exists("xdg-terminal-exec") {
|
||||
@@ -454,7 +449,14 @@ fn detect_terminal() -> String {
|
||||
}
|
||||
|
||||
// 5. Common X11/legacy terminals
|
||||
let legacy_terminals = ["gnome-terminal", "konsole", "xfce4-terminal", "mate-terminal", "tilix", "terminator"];
|
||||
let legacy_terminals = [
|
||||
"gnome-terminal",
|
||||
"konsole",
|
||||
"xfce4-terminal",
|
||||
"mate-terminal",
|
||||
"tilix",
|
||||
"terminator",
|
||||
];
|
||||
for term in legacy_terminals {
|
||||
if command_exists(term) {
|
||||
debug!("Found legacy terminal: {}", term);
|
||||
|
||||
@@ -94,7 +94,10 @@ impl ProviderFilter {
|
||||
};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Created with enabled providers: {:?}", filter.enabled);
|
||||
debug!(
|
||||
"[Filter] Created with enabled providers: {:?}",
|
||||
filter.enabled
|
||||
);
|
||||
|
||||
filter
|
||||
}
|
||||
@@ -118,13 +121,19 @@ impl ProviderFilter {
|
||||
self.enabled.insert(ProviderType::Application);
|
||||
}
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||
debug!(
|
||||
"[Filter] Toggled OFF {:?}, enabled: {:?}",
|
||||
provider, self.enabled
|
||||
);
|
||||
} else {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
let provider_debug = format!("{:?}", provider);
|
||||
self.enabled.insert(provider);
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled ON {}, enabled: {:?}", provider_debug, self.enabled);
|
||||
debug!(
|
||||
"[Filter] Toggled ON {}, enabled: {:?}",
|
||||
provider_debug, self.enabled
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,7 +160,10 @@ impl ProviderFilter {
|
||||
pub fn set_prefix(&mut self, prefix: Option<ProviderType>) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
if self.active_prefix != prefix {
|
||||
debug!("[Filter] Prefix changed: {:?} -> {:?}", self.active_prefix, prefix);
|
||||
debug!(
|
||||
"[Filter] Prefix changed: {:?} -> {:?}",
|
||||
self.active_prefix, prefix
|
||||
);
|
||||
}
|
||||
self.active_prefix = prefix;
|
||||
}
|
||||
@@ -190,7 +202,10 @@ impl ProviderFilter {
|
||||
let tag = rest[..space_idx].to_lowercase();
|
||||
let query_part = rest[space_idx + 1..].to_string();
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> tag={:?}, query={:?}", query, tag, query_part);
|
||||
debug!(
|
||||
"[Filter] parse_query({:?}) -> tag={:?}, query={:?}",
|
||||
query, tag, query_part
|
||||
);
|
||||
return ParsedQuery {
|
||||
prefix: None,
|
||||
tag_filter: Some(tag),
|
||||
@@ -245,7 +260,10 @@ impl ProviderFilter {
|
||||
for (prefix_str, provider) in core_prefixes {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||
debug!(
|
||||
"[Filter] parse_query({:?}) -> prefix={:?}, query={:?}",
|
||||
query, provider, rest
|
||||
);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider.clone()),
|
||||
tag_filter: None,
|
||||
@@ -259,7 +277,10 @@ impl ProviderFilter {
|
||||
if let Some(rest) = trimmed.strip_prefix(prefix_str) {
|
||||
let provider = ProviderType::Plugin(type_id.to_string());
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, query={:?}", query, provider, rest);
|
||||
debug!(
|
||||
"[Filter] parse_query({:?}) -> prefix={:?}, query={:?}",
|
||||
query, provider, rest
|
||||
);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
tag_filter: None,
|
||||
@@ -304,7 +325,10 @@ impl ProviderFilter {
|
||||
for (prefix_str, provider) in partial_core {
|
||||
if trimmed == *prefix_str {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||
debug!(
|
||||
"[Filter] parse_query({:?}) -> partial prefix {:?}",
|
||||
query, provider
|
||||
);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider.clone()),
|
||||
tag_filter: None,
|
||||
@@ -317,7 +341,10 @@ impl ProviderFilter {
|
||||
if trimmed == *prefix_str {
|
||||
let provider = ProviderType::Plugin(type_id.to_string());
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> partial prefix {:?}", query, provider);
|
||||
debug!(
|
||||
"[Filter] parse_query({:?}) -> partial prefix {:?}",
|
||||
query, provider
|
||||
);
|
||||
return ParsedQuery {
|
||||
prefix: Some(provider),
|
||||
tag_filter: None,
|
||||
@@ -333,7 +360,10 @@ impl ProviderFilter {
|
||||
};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] parse_query({:?}) -> prefix={:?}, tag={:?}, query={:?}", query, result.prefix, result.tag_filter, result.query);
|
||||
debug!(
|
||||
"[Filter] parse_query({:?}) -> prefix={:?}, tag={:?}, query={:?}",
|
||||
query, result.prefix, result.tag_filter, result.query
|
||||
);
|
||||
|
||||
result
|
||||
}
|
||||
@@ -396,7 +426,8 @@ impl ProviderFilter {
|
||||
/// "app"/"apps"/"application" -> Application, "cmd"/"command" -> Command,
|
||||
/// "dmenu" -> Dmenu, and everything else -> Plugin(id).
|
||||
pub fn mode_string_to_provider_type(mode: &str) -> ProviderType {
|
||||
mode.parse::<ProviderType>().unwrap_or_else(|_| ProviderType::Plugin(mode.to_string()))
|
||||
mode.parse::<ProviderType>()
|
||||
.unwrap_or_else(|_| ProviderType::Plugin(mode.to_string()))
|
||||
}
|
||||
|
||||
/// Get display name for current mode
|
||||
@@ -452,7 +483,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_parse_query_plugin_prefix() {
|
||||
let result = ProviderFilter::parse_query(":calc 5+3");
|
||||
assert_eq!(result.prefix, Some(ProviderType::Plugin("calc".to_string())));
|
||||
assert_eq!(
|
||||
result.prefix,
|
||||
Some(ProviderType::Plugin("calc".to_string()))
|
||||
);
|
||||
assert_eq!(result.query, "5+3");
|
||||
}
|
||||
|
||||
@@ -544,10 +578,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_explicit_mode_filter_rejects_unknown_plugins() {
|
||||
let filter = ProviderFilter::from_mode_strings(&[
|
||||
"app".to_string(),
|
||||
"cmd".to_string(),
|
||||
]);
|
||||
let filter = ProviderFilter::from_mode_strings(&["app".to_string(), "cmd".to_string()]);
|
||||
assert!(filter.is_active(ProviderType::Application));
|
||||
assert!(filter.is_active(ProviderType::Command));
|
||||
// Plugins not in the explicit list must be rejected
|
||||
|
||||
@@ -29,19 +29,11 @@ pub enum Request {
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
pub enum Response {
|
||||
Results {
|
||||
items: Vec<ResultItem>,
|
||||
},
|
||||
Providers {
|
||||
list: Vec<ProviderDesc>,
|
||||
},
|
||||
SubmenuItems {
|
||||
items: Vec<ResultItem>,
|
||||
},
|
||||
Results { items: Vec<ResultItem> },
|
||||
Providers { list: Vec<ProviderDesc> },
|
||||
SubmenuItems { items: Vec<ResultItem> },
|
||||
Ack,
|
||||
Error {
|
||||
message: String,
|
||||
},
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
|
||||
@@ -32,7 +32,6 @@ pub fn cache_home() -> Option<PathBuf> {
|
||||
dirs::cache_dir()
|
||||
}
|
||||
|
||||
|
||||
// =============================================================================
|
||||
// Owlry-specific directories
|
||||
// =============================================================================
|
||||
@@ -175,9 +174,10 @@ pub fn socket_path() -> PathBuf {
|
||||
/// Ensure parent directory of a file exists
|
||||
pub fn ensure_parent_dir(path: &std::path::Path) -> std::io::Result<()> {
|
||||
if let Some(parent) = path.parent()
|
||||
&& !parent.exists() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
&& !parent.exists()
|
||||
{
|
||||
std::fs::create_dir_all(parent)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -54,9 +54,9 @@ pub fn register_action_api(lua: &Lua, owlry: &Table, plugin_id: &str) -> LuaResu
|
||||
.get("name")
|
||||
.map_err(|_| mlua::Error::external("action.register: 'name' is required"))?;
|
||||
|
||||
let _handler: Function = config
|
||||
.get("handler")
|
||||
.map_err(|_| mlua::Error::external("action.register: 'handler' function is required"))?;
|
||||
let _handler: Function = config.get("handler").map_err(|_| {
|
||||
mlua::Error::external("action.register: 'handler' function is required")
|
||||
})?;
|
||||
|
||||
// Extract optional fields
|
||||
let icon: Option<String> = config.get("icon").ok();
|
||||
@@ -166,7 +166,7 @@ pub fn get_actions_for_item(lua: &Lua, item: &Table) -> LuaResult<Vec<ActionRegi
|
||||
// Check filter if present
|
||||
if let Ok(filter) = entry.get::<Function>("filter") {
|
||||
match filter.call::<bool>(item.clone()) {
|
||||
Ok(true) => {} // Include this action
|
||||
Ok(true) => {} // Include this action
|
||||
Ok(false) => continue, // Skip this action
|
||||
Err(e) => {
|
||||
log::warn!("Action filter failed: {}", e);
|
||||
@@ -220,7 +220,8 @@ mod tests {
|
||||
fn test_action_registration() {
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
return owlry.action.register({
|
||||
id = "copy-name",
|
||||
name = "Copy Name",
|
||||
@@ -229,7 +230,8 @@ mod tests {
|
||||
-- copy logic here
|
||||
end
|
||||
})
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let action_id: String = chunk.call(()).unwrap();
|
||||
assert_eq!(action_id, "test-plugin:copy-name");
|
||||
|
||||
@@ -243,7 +245,8 @@ mod tests {
|
||||
fn test_action_with_filter() {
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.action.register({
|
||||
id = "bookmark-action",
|
||||
name = "Open in Browser",
|
||||
@@ -252,7 +255,8 @@ mod tests {
|
||||
end,
|
||||
handler = function(item) end
|
||||
})
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
chunk.call::<()>(()).unwrap();
|
||||
|
||||
// Create bookmark item
|
||||
@@ -276,14 +280,16 @@ mod tests {
|
||||
fn test_action_unregister() {
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.action.register({
|
||||
id = "temp-action",
|
||||
name = "Temporary",
|
||||
handler = function(item) end
|
||||
})
|
||||
return owlry.action.unregister("temp-action")
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let unregistered: bool = chunk.call(()).unwrap();
|
||||
assert!(unregistered);
|
||||
|
||||
@@ -296,7 +302,8 @@ mod tests {
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
// Register action that sets a global
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
result = nil
|
||||
owlry.action.register({
|
||||
id = "test-exec",
|
||||
@@ -305,7 +312,8 @@ mod tests {
|
||||
result = item.name
|
||||
end
|
||||
})
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
chunk.call::<()>(()).unwrap();
|
||||
|
||||
// Create test item
|
||||
|
||||
@@ -35,9 +35,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
cache_table.set(
|
||||
"get",
|
||||
lua.create_function(|lua, key: String| {
|
||||
let cache = CACHE.lock().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to lock cache: {}", e))
|
||||
})?;
|
||||
let cache = CACHE
|
||||
.lock()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
|
||||
|
||||
if let Some(entry) = cache.get(&key) {
|
||||
if entry.is_expired() {
|
||||
@@ -50,8 +50,10 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
}
|
||||
|
||||
// Parse JSON back to Lua value
|
||||
let json_value: serde_json::Value = serde_json::from_str(&entry.value)
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to parse cached value: {}", e)))?;
|
||||
let json_value: serde_json::Value =
|
||||
serde_json::from_str(&entry.value).map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to parse cached value: {}", e))
|
||||
})?;
|
||||
|
||||
json_to_lua(lua, &json_value)
|
||||
} else {
|
||||
@@ -75,9 +77,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
expires_at,
|
||||
};
|
||||
|
||||
let mut cache = CACHE.lock().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to lock cache: {}", e))
|
||||
})?;
|
||||
let mut cache = CACHE
|
||||
.lock()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
|
||||
|
||||
cache.insert(key, entry);
|
||||
Ok(true)
|
||||
@@ -88,9 +90,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
cache_table.set(
|
||||
"delete",
|
||||
lua.create_function(|_lua, key: String| {
|
||||
let mut cache = CACHE.lock().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to lock cache: {}", e))
|
||||
})?;
|
||||
let mut cache = CACHE
|
||||
.lock()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
|
||||
|
||||
Ok(cache.remove(&key).is_some())
|
||||
})?,
|
||||
@@ -100,9 +102,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
cache_table.set(
|
||||
"clear",
|
||||
lua.create_function(|_lua, ()| {
|
||||
let mut cache = CACHE.lock().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to lock cache: {}", e))
|
||||
})?;
|
||||
let mut cache = CACHE
|
||||
.lock()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
|
||||
|
||||
let count = cache.len();
|
||||
cache.clear();
|
||||
@@ -114,9 +116,9 @@ pub fn register_cache_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
cache_table.set(
|
||||
"has",
|
||||
lua.create_function(|_lua, key: String| {
|
||||
let cache = CACHE.lock().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to lock cache: {}", e))
|
||||
})?;
|
||||
let cache = CACHE
|
||||
.lock()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to lock cache: {}", e)))?;
|
||||
|
||||
if let Some(entry) = cache.get(&key) {
|
||||
Ok(!entry.is_expired())
|
||||
@@ -249,10 +251,12 @@ mod tests {
|
||||
let _: bool = chunk.call(()).unwrap();
|
||||
|
||||
// Get and verify
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
local t = owlry.cache.get("table_key")
|
||||
return t.name, t.value
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let (name, value): (String, i32) = chunk.call(()).unwrap();
|
||||
assert_eq!(name, "test");
|
||||
assert_eq!(value, 42);
|
||||
@@ -262,12 +266,14 @@ mod tests {
|
||||
fn test_cache_delete() {
|
||||
let lua = setup_lua();
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.cache.set("delete_key", "value")
|
||||
local existed = owlry.cache.delete("delete_key")
|
||||
local value = owlry.cache.get("delete_key")
|
||||
return existed, value
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let (existed, value): (bool, Option<String>) = chunk.call(()).unwrap();
|
||||
assert!(existed);
|
||||
assert!(value.is_none());
|
||||
@@ -277,12 +283,14 @@ mod tests {
|
||||
fn test_cache_has() {
|
||||
let lua = setup_lua();
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
local before = owlry.cache.has("has_key")
|
||||
owlry.cache.set("has_key", "value")
|
||||
local after = owlry.cache.has("has_key")
|
||||
return before, after
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let (before, after): (bool, bool) = chunk.call(()).unwrap();
|
||||
assert!(!before);
|
||||
assert!(after);
|
||||
|
||||
@@ -329,13 +329,15 @@ mod tests {
|
||||
clear_all_hooks();
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
local called = false
|
||||
owlry.hook.on("init", function()
|
||||
called = true
|
||||
end)
|
||||
return true
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let result: bool = chunk.call(()).unwrap();
|
||||
assert!(result);
|
||||
|
||||
@@ -349,11 +351,13 @@ mod tests {
|
||||
clear_all_hooks();
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.hook.on("query", function(q) return q .. "1" end, 10)
|
||||
owlry.hook.on("query", function(q) return q .. "2" end, 20)
|
||||
return true
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
chunk.call::<()>(()).unwrap();
|
||||
|
||||
// Call hooks - higher priority (20) should run first
|
||||
@@ -367,11 +371,13 @@ mod tests {
|
||||
clear_all_hooks();
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.hook.on("select", function() end)
|
||||
owlry.hook.off("select")
|
||||
return true
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
chunk.call::<()>(()).unwrap();
|
||||
|
||||
let plugins = get_registered_plugins(HookEvent::Select);
|
||||
@@ -383,14 +389,16 @@ mod tests {
|
||||
clear_all_hooks();
|
||||
let lua = setup_lua("test-plugin");
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.hook.on("pre_launch", function(item)
|
||||
if item.name == "blocked" then
|
||||
return false -- cancel launch
|
||||
end
|
||||
return true
|
||||
end)
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
chunk.call::<()>(()).unwrap();
|
||||
|
||||
// Create a test item table
|
||||
|
||||
@@ -26,18 +26,21 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.build()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to create HTTP client: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to create HTTP client: {}", e))
|
||||
})?;
|
||||
|
||||
let mut request = client.get(&url);
|
||||
|
||||
// Add custom headers if provided
|
||||
if let Some(ref opts) = opts
|
||||
&& let Ok(headers) = opts.get::<Table>("headers") {
|
||||
for pair in headers.pairs::<String, String>() {
|
||||
let (key, value) = pair?;
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
&& let Ok(headers) = opts.get::<Table>("headers")
|
||||
{
|
||||
for pair in headers.pairs::<String, String>() {
|
||||
let (key, value) = pair?;
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
@@ -45,9 +48,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
|
||||
let status = response.status().as_u16();
|
||||
let headers = extract_headers(&response);
|
||||
let body = response
|
||||
.text()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?;
|
||||
let body = response.text().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to read response body: {}", e))
|
||||
})?;
|
||||
|
||||
let result = lua.create_table()?;
|
||||
result.set("status", status)?;
|
||||
@@ -78,18 +81,21 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.build()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to create HTTP client: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to create HTTP client: {}", e))
|
||||
})?;
|
||||
|
||||
let mut request = client.post(&url);
|
||||
|
||||
// Add custom headers if provided
|
||||
if let Some(ref opts) = opts
|
||||
&& let Ok(headers) = opts.get::<Table>("headers") {
|
||||
for pair in headers.pairs::<String, String>() {
|
||||
let (key, value) = pair?;
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
&& let Ok(headers) = opts.get::<Table>("headers")
|
||||
{
|
||||
for pair in headers.pairs::<String, String>() {
|
||||
let (key, value) = pair?;
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
// Set body based on type
|
||||
request = match body {
|
||||
@@ -102,11 +108,7 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
.body(json_str)
|
||||
}
|
||||
Value::Nil => request,
|
||||
_ => {
|
||||
return Err(mlua::Error::external(
|
||||
"POST body must be a string or table",
|
||||
))
|
||||
}
|
||||
_ => return Err(mlua::Error::external("POST body must be a string or table")),
|
||||
};
|
||||
|
||||
let response = request
|
||||
@@ -115,9 +117,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
|
||||
let status = response.status().as_u16();
|
||||
let headers = extract_headers(&response);
|
||||
let body = response
|
||||
.text()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?;
|
||||
let body = response.text().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to read response body: {}", e))
|
||||
})?;
|
||||
|
||||
let result = lua.create_table()?;
|
||||
result.set("status", status)?;
|
||||
@@ -149,19 +151,22 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let client = reqwest::blocking::Client::builder()
|
||||
.timeout(Duration::from_secs(timeout_secs))
|
||||
.build()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to create HTTP client: {}", e)))?;
|
||||
.map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to create HTTP client: {}", e))
|
||||
})?;
|
||||
|
||||
let mut request = client.get(&url);
|
||||
request = request.header("Accept", "application/json");
|
||||
|
||||
// Add custom headers if provided
|
||||
if let Some(ref opts) = opts
|
||||
&& let Ok(headers) = opts.get::<Table>("headers") {
|
||||
for pair in headers.pairs::<String, String>() {
|
||||
let (key, value) = pair?;
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
&& let Ok(headers) = opts.get::<Table>("headers")
|
||||
{
|
||||
for pair in headers.pairs::<String, String>() {
|
||||
let (key, value) = pair?;
|
||||
request = request.header(&key, &value);
|
||||
}
|
||||
}
|
||||
|
||||
let response = request
|
||||
.send()
|
||||
@@ -174,9 +179,9 @@ pub fn register_http_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
)));
|
||||
}
|
||||
|
||||
let body = response
|
||||
.text()
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to read response body: {}", e)))?;
|
||||
let body = response.text().map_err(|e| {
|
||||
mlua::Error::external(format!("Failed to read response body: {}", e))
|
||||
})?;
|
||||
|
||||
// Parse JSON and convert to Lua table
|
||||
let json_value: serde_json::Value = serde_json::from_str(&body)
|
||||
|
||||
@@ -14,20 +14,20 @@ pub fn register_math_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
// Returns (result, nil) on success or (nil, error_message) on failure
|
||||
math_table.set(
|
||||
"calculate",
|
||||
lua.create_function(|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> {
|
||||
match meval::eval_str(&expr) {
|
||||
Ok(result) => {
|
||||
if result.is_finite() {
|
||||
Ok((Some(result), None))
|
||||
} else {
|
||||
Ok((None, Some("Result is not a finite number".to_string())))
|
||||
lua.create_function(
|
||||
|_lua, expr: String| -> LuaResult<(Option<f64>, Option<String>)> {
|
||||
match meval::eval_str(&expr) {
|
||||
Ok(result) => {
|
||||
if result.is_finite() {
|
||||
Ok((Some(result), None))
|
||||
} else {
|
||||
Ok((None, Some("Result is not a finite number".to_string())))
|
||||
}
|
||||
}
|
||||
Err(e) => Ok((None, Some(e.to_string()))),
|
||||
}
|
||||
Err(e) => {
|
||||
Ok((None, Some(e.to_string())))
|
||||
}
|
||||
}
|
||||
})?,
|
||||
},
|
||||
)?,
|
||||
)?;
|
||||
|
||||
// owlry.math.calc(expression) -> number (throws on error)
|
||||
@@ -106,11 +106,13 @@ mod tests {
|
||||
fn test_calculate_basic() {
|
||||
let lua = setup_lua();
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
local result, err = owlry.math.calculate("2 + 2")
|
||||
if err then error(err) end
|
||||
return result
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let result: f64 = chunk.call(()).unwrap();
|
||||
assert!((result - 4.0).abs() < f64::EPSILON);
|
||||
}
|
||||
@@ -119,11 +121,13 @@ mod tests {
|
||||
fn test_calculate_complex() {
|
||||
let lua = setup_lua();
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
local result, err = owlry.math.calculate("sqrt(16) + 2^3")
|
||||
if err then error(err) end
|
||||
return result
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let result: f64 = chunk.call(()).unwrap();
|
||||
assert!((result - 12.0).abs() < f64::EPSILON); // sqrt(16) = 4, 2^3 = 8
|
||||
}
|
||||
@@ -132,14 +136,16 @@ mod tests {
|
||||
fn test_calculate_error() {
|
||||
let lua = setup_lua();
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
local result, err = owlry.math.calculate("invalid expression @@")
|
||||
if result then
|
||||
return false -- should not succeed
|
||||
else
|
||||
return true -- correctly failed
|
||||
end
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let had_error: bool = chunk.call(()).unwrap();
|
||||
assert!(had_error);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,14 @@ pub fn register_process_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
.map_err(|e| mlua::Error::external(format!("Failed to run command: {}", e)))?;
|
||||
|
||||
let result = lua.create_table()?;
|
||||
result.set("stdout", String::from_utf8_lossy(&output.stdout).to_string())?;
|
||||
result.set("stderr", String::from_utf8_lossy(&output.stderr).to_string())?;
|
||||
result.set(
|
||||
"stdout",
|
||||
String::from_utf8_lossy(&output.stdout).to_string(),
|
||||
)?;
|
||||
result.set(
|
||||
"stderr",
|
||||
String::from_utf8_lossy(&output.stderr).to_string(),
|
||||
)?;
|
||||
result.set("exit_code", output.status.code().unwrap_or(-1))?;
|
||||
result.set("success", output.status.success())?;
|
||||
|
||||
@@ -95,9 +101,7 @@ pub fn register_env_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
// owlry.env.get(name) -> string or nil
|
||||
env_table.set(
|
||||
"get",
|
||||
lua.create_function(|_lua, name: String| {
|
||||
Ok(std::env::var(&name).ok())
|
||||
})?,
|
||||
lua.create_function(|_lua, name: String| Ok(std::env::var(&name).ok()))?,
|
||||
)?;
|
||||
|
||||
// owlry.env.get_or(name, default) -> string
|
||||
@@ -166,7 +170,8 @@ mod tests {
|
||||
assert!(exists);
|
||||
|
||||
// Made-up command should not exist
|
||||
let chunk = lua.load(r#"return owlry.process.exists("this_command_definitely_does_not_exist_12345")"#);
|
||||
let chunk = lua
|
||||
.load(r#"return owlry.process.exists("this_command_definitely_does_not_exist_12345")"#);
|
||||
let not_exists: bool = chunk.call(()).unwrap();
|
||||
assert!(!not_exists);
|
||||
}
|
||||
@@ -190,7 +195,8 @@ mod tests {
|
||||
fn test_env_get_or() {
|
||||
let lua = setup_lua();
|
||||
|
||||
let chunk = lua.load(r#"return owlry.env.get_or("THIS_VAR_DOES_NOT_EXIST_12345", "default_value")"#);
|
||||
let chunk = lua
|
||||
.load(r#"return owlry.env.get_or("THIS_VAR_DOES_NOT_EXIST_12345", "default_value")"#);
|
||||
let result: String = chunk.call(()).unwrap();
|
||||
assert_eq!(result, "default_value");
|
||||
}
|
||||
|
||||
@@ -21,7 +21,12 @@ pub struct ThemeRegistration {
|
||||
}
|
||||
|
||||
/// Register theme APIs
|
||||
pub fn register_theme_api(lua: &Lua, owlry: &Table, plugin_id: &str, plugin_dir: &Path) -> LuaResult<()> {
|
||||
pub fn register_theme_api(
|
||||
lua: &Lua,
|
||||
owlry: &Table,
|
||||
plugin_id: &str,
|
||||
plugin_dir: &Path,
|
||||
) -> LuaResult<()> {
|
||||
let theme_table = lua.create_table()?;
|
||||
let plugin_id_owned = plugin_id.to_string();
|
||||
let plugin_dir_owned = plugin_dir.to_path_buf();
|
||||
@@ -50,9 +55,7 @@ pub fn register_theme_api(lua: &Lua, owlry: &Table, plugin_id: &str, plugin_dir:
|
||||
.get("name")
|
||||
.map_err(|_| mlua::Error::external("theme.register: 'name' is required"))?;
|
||||
|
||||
let display_name: String = config
|
||||
.get("display_name")
|
||||
.unwrap_or_else(|_| name.clone());
|
||||
let display_name: String = config.get("display_name").unwrap_or_else(|_| name.clone());
|
||||
|
||||
// Get CSS either directly or from file
|
||||
let css: String = if let Ok(css_str) = config.get::<String>("css") {
|
||||
@@ -197,13 +200,15 @@ mod tests {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let lua = setup_lua("test-plugin", temp.path());
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
return owlry.theme.register({
|
||||
name = "my-theme",
|
||||
display_name = "My Theme",
|
||||
css = ".owlry-window { background: #333; }"
|
||||
})
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let name: String = chunk.call(()).unwrap();
|
||||
assert_eq!(name, "my-theme");
|
||||
|
||||
@@ -221,12 +226,14 @@ mod tests {
|
||||
|
||||
let lua = setup_lua("test-plugin", temp.path());
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
return owlry.theme.register({
|
||||
name = "file-theme",
|
||||
css_file = "theme.css"
|
||||
})
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let name: String = chunk.call(()).unwrap();
|
||||
assert_eq!(name, "file-theme");
|
||||
|
||||
@@ -240,11 +247,13 @@ mod tests {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let lua = setup_lua("test-plugin", temp.path());
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.theme.register({ name = "theme1", css = "a{}" })
|
||||
owlry.theme.register({ name = "theme2", css = "b{}" })
|
||||
return owlry.theme.list()
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let list: Table = chunk.call(()).unwrap();
|
||||
|
||||
let mut names: Vec<String> = Vec::new();
|
||||
@@ -262,10 +271,12 @@ mod tests {
|
||||
let temp = TempDir::new().unwrap();
|
||||
let lua = setup_lua("test-plugin", temp.path());
|
||||
|
||||
let chunk = lua.load(r#"
|
||||
let chunk = lua.load(
|
||||
r#"
|
||||
owlry.theme.register({ name = "temp-theme", css = "c{}" })
|
||||
return owlry.theme.unregister("temp-theme")
|
||||
"#);
|
||||
"#,
|
||||
);
|
||||
let unregistered: bool = chunk.call(()).unwrap();
|
||||
assert!(unregistered);
|
||||
|
||||
|
||||
@@ -189,9 +189,10 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResult
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = full_path.parent()
|
||||
&& !parent.exists()
|
||||
&& let Err(e) = std::fs::create_dir_all(parent) {
|
||||
return Ok((false, Value::String(lua.create_string(e.to_string())?)));
|
||||
}
|
||||
&& let Err(e) = std::fs::create_dir_all(parent)
|
||||
{
|
||||
return Ok((false, Value::String(lua.create_string(e.to_string())?)));
|
||||
}
|
||||
|
||||
match std::fs::write(&full_path, content) {
|
||||
Ok(()) => Ok((true, Value::Nil)),
|
||||
@@ -295,7 +296,8 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResult
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let plugin_dir: String = lua.named_registry_value("plugin_dir")?;
|
||||
let full_path = resolve_plugin_path(&plugin_dir, &path);
|
||||
let is_exec = full_path.metadata()
|
||||
let is_exec = full_path
|
||||
.metadata()
|
||||
.map(|m| m.permissions().mode() & 0o111 != 0)
|
||||
.unwrap_or(false);
|
||||
Ok(is_exec)
|
||||
@@ -335,28 +337,24 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
// owlry.json.encode(value) -> string or nil, error
|
||||
json_table.set(
|
||||
"encode",
|
||||
lua.create_function(|lua, value: Value| {
|
||||
match lua_to_json(&value) {
|
||||
Ok(json) => match serde_json::to_string(&json) {
|
||||
Ok(s) => Ok((Some(s), Value::Nil)),
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
|
||||
},
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
|
||||
}
|
||||
lua.create_function(|lua, value: Value| match lua_to_json(&value) {
|
||||
Ok(json) => match serde_json::to_string(&json) {
|
||||
Ok(s) => Ok((Some(s), Value::Nil)),
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
|
||||
},
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.json.encode_pretty(value) -> string or nil, error
|
||||
json_table.set(
|
||||
"encode_pretty",
|
||||
lua.create_function(|lua, value: Value| {
|
||||
match lua_to_json(&value) {
|
||||
Ok(json) => match serde_json::to_string_pretty(&json) {
|
||||
Ok(s) => Ok((Some(s), Value::Nil)),
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
|
||||
},
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
|
||||
}
|
||||
lua.create_function(|lua, value: Value| match lua_to_json(&value) {
|
||||
Ok(json) => match serde_json::to_string_pretty(&json) {
|
||||
Ok(s) => Ok((Some(s), Value::Nil)),
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(e.to_string())?))),
|
||||
},
|
||||
Err(e) => Ok((None, Value::String(lua.create_string(&e)?))),
|
||||
})?,
|
||||
)?;
|
||||
|
||||
@@ -388,13 +386,16 @@ fn lua_to_json(value: &Value) -> Result<serde_json::Value, String> {
|
||||
.map(serde_json::Value::Number)
|
||||
.ok_or_else(|| "Invalid number".to_string()),
|
||||
Value::String(s) => Ok(serde_json::Value::String(
|
||||
s.to_str().map_err(|e| e.to_string())?.to_string()
|
||||
s.to_str().map_err(|e| e.to_string())?.to_string(),
|
||||
)),
|
||||
Value::Table(t) => {
|
||||
// Check if it's an array (sequential integer keys starting from 1)
|
||||
let len = t.raw_len();
|
||||
let is_array = len > 0
|
||||
&& (1..=len).all(|i| t.raw_get::<Value>(i).is_ok_and(|v| !matches!(v, Value::Nil)));
|
||||
&& (1..=len).all(|i| {
|
||||
t.raw_get::<Value>(i)
|
||||
.is_ok_and(|v| !matches!(v, Value::Nil))
|
||||
});
|
||||
|
||||
if is_array {
|
||||
let arr: Result<Vec<serde_json::Value>, String> = (1..=len)
|
||||
@@ -475,9 +476,13 @@ mod tests {
|
||||
fn test_log_api() {
|
||||
let (lua, _temp) = create_test_lua();
|
||||
// Just verify it doesn't panic - using call instead of the e-word
|
||||
lua.load("owlry.log.info('test message')").call::<()>(()).unwrap();
|
||||
lua.load("owlry.log.info('test message')")
|
||||
.call::<()>(())
|
||||
.unwrap();
|
||||
lua.load("owlry.log.debug('debug')").call::<()>(()).unwrap();
|
||||
lua.load("owlry.log.warn('warning')").call::<()>(()).unwrap();
|
||||
lua.load("owlry.log.warn('warning')")
|
||||
.call::<()>(())
|
||||
.unwrap();
|
||||
lua.load("owlry.log.error('error')").call::<()>(()).unwrap();
|
||||
}
|
||||
|
||||
@@ -485,10 +490,7 @@ mod tests {
|
||||
fn test_path_api() {
|
||||
let (lua, _temp) = create_test_lua();
|
||||
|
||||
let home: String = lua
|
||||
.load("return owlry.path.home()")
|
||||
.call(())
|
||||
.unwrap();
|
||||
let home: String = lua.load("return owlry.path.home()").call(()).unwrap();
|
||||
assert!(!home.is_empty());
|
||||
|
||||
let joined: String = lua
|
||||
|
||||
@@ -7,7 +7,7 @@ use mlua::Lua;
|
||||
use super::api;
|
||||
use super::error::{PluginError, PluginResult};
|
||||
use super::manifest::PluginManifest;
|
||||
use super::runtime::{create_lua_runtime, load_file, SandboxConfig};
|
||||
use super::runtime::{SandboxConfig, create_lua_runtime, load_file};
|
||||
|
||||
/// A loaded plugin instance
|
||||
#[derive(Debug)]
|
||||
@@ -94,7 +94,10 @@ impl LoadedPlugin {
|
||||
}
|
||||
|
||||
/// Call a provider's refresh function
|
||||
pub fn call_provider_refresh(&self, provider_name: &str) -> PluginResult<Vec<super::PluginItem>> {
|
||||
pub fn call_provider_refresh(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
) -> PluginResult<Vec<super::PluginItem>> {
|
||||
let lua = self.lua.as_ref().ok_or_else(|| PluginError::LuaError {
|
||||
plugin: self.id().to_string(),
|
||||
message: "Plugin not initialized".to_string(),
|
||||
@@ -108,7 +111,11 @@ impl LoadedPlugin {
|
||||
|
||||
/// Call a provider's query function
|
||||
#[allow(dead_code)] // Will be used for dynamic query providers
|
||||
pub fn call_provider_query(&self, provider_name: &str, query: &str) -> PluginResult<Vec<super::PluginItem>> {
|
||||
pub fn call_provider_query(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
query: &str,
|
||||
) -> PluginResult<Vec<super::PluginItem>> {
|
||||
let lua = self.lua.as_ref().ok_or_else(|| PluginError::LuaError {
|
||||
plugin: self.id().to_string(),
|
||||
message: "Plugin not initialized".to_string(),
|
||||
@@ -138,8 +145,8 @@ impl LoadedPlugin {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use super::super::manifest::{check_compatibility, discover_plugins};
|
||||
use super::*;
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use tempfile::TempDir;
|
||||
|
||||
@@ -112,11 +112,16 @@ pub struct PluginPermissions {
|
||||
/// Discover all plugins in a directory
|
||||
///
|
||||
/// Returns a map of plugin ID -> (manifest, path)
|
||||
pub fn discover_plugins(plugins_dir: &Path) -> PluginResult<HashMap<String, (PluginManifest, PathBuf)>> {
|
||||
pub fn discover_plugins(
|
||||
plugins_dir: &Path,
|
||||
) -> PluginResult<HashMap<String, (PluginManifest, PathBuf)>> {
|
||||
let mut plugins = HashMap::new();
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
log::debug!("Plugins directory does not exist: {}", plugins_dir.display());
|
||||
log::debug!(
|
||||
"Plugins directory does not exist: {}",
|
||||
plugins_dir.display()
|
||||
);
|
||||
return Ok(plugins);
|
||||
}
|
||||
|
||||
@@ -143,7 +148,11 @@ pub fn discover_plugins(plugins_dir: &Path) -> PluginResult<HashMap<String, (Plu
|
||||
log::warn!("Duplicate plugin ID '{}', skipping {}", id, path.display());
|
||||
continue;
|
||||
}
|
||||
log::info!("Discovered plugin: {} v{}", manifest.plugin.name, manifest.plugin.version);
|
||||
log::info!(
|
||||
"Discovered plugin: {} v{}",
|
||||
manifest.plugin.name,
|
||||
manifest.plugin.version
|
||||
);
|
||||
plugins.insert(id, (manifest, path));
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -204,7 +213,12 @@ impl PluginManifest {
|
||||
});
|
||||
}
|
||||
|
||||
if !self.plugin.id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
|
||||
if !self
|
||||
.plugin
|
||||
.id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
return Err(PluginError::InvalidManifest {
|
||||
plugin: self.plugin.id.clone(),
|
||||
message: "Plugin ID must be lowercase alphanumeric with hyphens".to_string(),
|
||||
@@ -223,7 +237,10 @@ impl PluginManifest {
|
||||
if semver::VersionReq::parse(&self.plugin.owlry_version).is_err() {
|
||||
return Err(PluginError::InvalidManifest {
|
||||
plugin: self.plugin.id.clone(),
|
||||
message: format!("Invalid owlry_version constraint: {}", self.plugin.owlry_version),
|
||||
message: format!(
|
||||
"Invalid owlry_version constraint: {}",
|
||||
self.plugin.owlry_version
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ pub use loader::LoadedPlugin;
|
||||
|
||||
// Used by plugins/commands.rs for plugin CLI commands
|
||||
#[allow(unused_imports)]
|
||||
pub use manifest::{check_compatibility, discover_plugins, PluginManifest};
|
||||
pub use manifest::{PluginManifest, check_compatibility, discover_plugins};
|
||||
|
||||
// ============================================================================
|
||||
// Lua Plugin Manager (only available with lua feature)
|
||||
@@ -64,7 +64,7 @@ mod lua_manager {
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
|
||||
use manifest::{discover_plugins, check_compatibility};
|
||||
use manifest::{check_compatibility, discover_plugins};
|
||||
|
||||
/// Plugin manager coordinates loading, initialization, and lifecycle of Lua plugins
|
||||
pub struct PluginManager {
|
||||
@@ -158,7 +158,10 @@ mod lua_manager {
|
||||
|
||||
/// Get all enabled plugins
|
||||
pub fn enabled_plugins(&self) -> impl Iterator<Item = Rc<RefCell<LoadedPlugin>>> + '_ {
|
||||
self.plugins.values().filter(|p| p.borrow().enabled).cloned()
|
||||
self.plugins
|
||||
.values()
|
||||
.filter(|p| p.borrow().enabled)
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Get the number of loaded plugins
|
||||
@@ -176,7 +179,10 @@ mod lua_manager {
|
||||
/// Enable a plugin by ID
|
||||
#[allow(dead_code)]
|
||||
pub fn enable(&mut self, id: &str) -> PluginResult<()> {
|
||||
let plugin_rc = self.plugins.get(id).ok_or_else(|| PluginError::NotFound(id.to_string()))?;
|
||||
let plugin_rc = self
|
||||
.plugins
|
||||
.get(id)
|
||||
.ok_or_else(|| PluginError::NotFound(id.to_string()))?;
|
||||
let mut plugin = plugin_rc.borrow_mut();
|
||||
|
||||
if !plugin.enabled {
|
||||
@@ -191,7 +197,10 @@ mod lua_manager {
|
||||
/// Disable a plugin by ID
|
||||
#[allow(dead_code)]
|
||||
pub fn disable(&mut self, id: &str) -> PluginResult<()> {
|
||||
let plugin_rc = self.plugins.get(id).ok_or_else(|| PluginError::NotFound(id.to_string()))?;
|
||||
let plugin_rc = self
|
||||
.plugins
|
||||
.get(id)
|
||||
.ok_or_else(|| PluginError::NotFound(id.to_string()))?;
|
||||
plugin_rc.borrow_mut().enabled = false;
|
||||
Ok(())
|
||||
}
|
||||
@@ -200,7 +209,13 @@ mod lua_manager {
|
||||
#[allow(dead_code)]
|
||||
pub fn providers_for(&self, provider_name: &str) -> Vec<String> {
|
||||
self.enabled_plugins()
|
||||
.filter(|p| p.borrow().manifest.provides.providers.contains(&provider_name.to_string()))
|
||||
.filter(|p| {
|
||||
p.borrow()
|
||||
.manifest
|
||||
.provides
|
||||
.providers
|
||||
.contains(&provider_name.to_string())
|
||||
})
|
||||
.map(|p| p.borrow().id().to_string())
|
||||
.collect()
|
||||
}
|
||||
@@ -208,13 +223,15 @@ mod lua_manager {
|
||||
/// Check if any plugin provides actions
|
||||
#[allow(dead_code)]
|
||||
pub fn has_action_plugins(&self) -> bool {
|
||||
self.enabled_plugins().any(|p| p.borrow().manifest.provides.actions)
|
||||
self.enabled_plugins()
|
||||
.any(|p| p.borrow().manifest.provides.actions)
|
||||
}
|
||||
|
||||
/// Check if any plugin provides hooks
|
||||
#[allow(dead_code)]
|
||||
pub fn has_hook_plugins(&self) -> bool {
|
||||
self.enabled_plugins().any(|p| p.borrow().manifest.provides.hooks)
|
||||
self.enabled_plugins()
|
||||
.any(|p| p.borrow().manifest.provides.hooks)
|
||||
}
|
||||
|
||||
/// Get all theme names provided by plugins
|
||||
|
||||
@@ -17,8 +17,8 @@ use std::sync::{Arc, Once};
|
||||
use libloading::Library;
|
||||
use log::{debug, error, info, warn};
|
||||
use owlry_plugin_api::{
|
||||
HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo, ProviderKind,
|
||||
RStr, API_VERSION,
|
||||
API_VERSION, HostAPI, NotifyUrgency, PluginInfo, PluginVTable, ProviderHandle, ProviderInfo,
|
||||
ProviderKind, RStr,
|
||||
};
|
||||
|
||||
use crate::notify;
|
||||
@@ -28,9 +28,18 @@ use crate::notify;
|
||||
// ============================================================================
|
||||
|
||||
/// Host notification handler
|
||||
extern "C" fn host_notify(summary: RStr<'_>, body: RStr<'_>, icon: RStr<'_>, urgency: NotifyUrgency) {
|
||||
extern "C" fn host_notify(
|
||||
summary: RStr<'_>,
|
||||
body: RStr<'_>,
|
||||
icon: RStr<'_>,
|
||||
urgency: NotifyUrgency,
|
||||
) {
|
||||
let icon_str = icon.as_str();
|
||||
let icon_opt = if icon_str.is_empty() { None } else { Some(icon_str) };
|
||||
let icon_opt = if icon_str.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(icon_str)
|
||||
};
|
||||
|
||||
let notify_urgency = match urgency {
|
||||
NotifyUrgency::Low => notify::NotifyUrgency::Low,
|
||||
@@ -121,7 +130,9 @@ impl NativePlugin {
|
||||
handle: ProviderHandle,
|
||||
query: &str,
|
||||
) -> Vec<owlry_plugin_api::PluginItem> {
|
||||
(self.vtable.provider_query)(handle, query.into()).into_iter().collect()
|
||||
(self.vtable.provider_query)(handle, query.into())
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Drop a provider handle
|
||||
|
||||
@@ -110,9 +110,10 @@ impl RegistryClient {
|
||||
|
||||
if let Ok(metadata) = fs::metadata(&cache_path)
|
||||
&& let Ok(modified) = metadata.modified()
|
||||
&& let Ok(elapsed) = SystemTime::now().duration_since(modified) {
|
||||
return elapsed < CACHE_DURATION;
|
||||
}
|
||||
&& let Ok(elapsed) = SystemTime::now().duration_since(modified)
|
||||
{
|
||||
return elapsed < CACHE_DURATION;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
@@ -120,11 +121,13 @@ impl RegistryClient {
|
||||
/// Fetch the registry index (from cache or network)
|
||||
pub fn fetch_index(&self, force_refresh: bool) -> Result<RegistryIndex, String> {
|
||||
// Use cache if valid and not forcing refresh
|
||||
if !force_refresh && self.is_cache_valid()
|
||||
if !force_refresh
|
||||
&& self.is_cache_valid()
|
||||
&& let Ok(content) = fs::read_to_string(self.cache_path())
|
||||
&& let Ok(index) = toml::from_str(&content) {
|
||||
return Ok(index);
|
||||
}
|
||||
&& let Ok(index) = toml::from_str(&content)
|
||||
{
|
||||
return Ok(index);
|
||||
}
|
||||
|
||||
// Fetch from network
|
||||
self.fetch_from_network()
|
||||
@@ -134,12 +137,7 @@ impl RegistryClient {
|
||||
fn fetch_from_network(&self) -> Result<RegistryIndex, String> {
|
||||
// Use curl for fetching (available on most systems)
|
||||
let output = std::process::Command::new("curl")
|
||||
.args([
|
||||
"-fsSL",
|
||||
"--max-time",
|
||||
"30",
|
||||
&self.registry_url,
|
||||
])
|
||||
.args(["-fsSL", "--max-time", "30", &self.registry_url])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to run curl: {}", e))?;
|
||||
|
||||
@@ -185,7 +183,9 @@ impl RegistryClient {
|
||||
p.id.to_lowercase().contains(&query_lower)
|
||||
|| p.name.to_lowercase().contains(&query_lower)
|
||||
|| p.description.to_lowercase().contains(&query_lower)
|
||||
|| p.tags.iter().any(|t| t.to_lowercase().contains(&query_lower))
|
||||
|| p.tags
|
||||
.iter()
|
||||
.any(|t| t.to_lowercase().contains(&query_lower))
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -210,8 +210,7 @@ impl RegistryClient {
|
||||
pub fn clear_cache(&self) -> Result<(), String> {
|
||||
let cache_path = self.cache_path();
|
||||
if cache_path.exists() {
|
||||
fs::remove_file(&cache_path)
|
||||
.map_err(|e| format!("Failed to remove cache: {}", e))?;
|
||||
fs::remove_file(&cache_path).map_err(|e| format!("Failed to remove cache: {}", e))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ impl Default for SandboxConfig {
|
||||
allow_commands: false,
|
||||
allow_network: false,
|
||||
allow_external_fs: false,
|
||||
max_run_time_ms: 5000, // 5 seconds
|
||||
max_run_time_ms: 5000, // 5 seconds
|
||||
max_memory: 64 * 1024 * 1024, // 64 MB
|
||||
}
|
||||
}
|
||||
@@ -49,11 +49,7 @@ pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> {
|
||||
// Create Lua with safe standard libraries only
|
||||
// ALL_SAFE excludes: debug, io, os (dangerous parts), package (loadlib), ffi
|
||||
// We then customize the os table to only allow safe functions
|
||||
let libs = StdLib::COROUTINE
|
||||
| StdLib::TABLE
|
||||
| StdLib::STRING
|
||||
| StdLib::UTF8
|
||||
| StdLib::MATH;
|
||||
let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
|
||||
|
||||
let lua = Lua::new_with(libs, mlua::LuaOptions::default())?;
|
||||
|
||||
@@ -75,9 +71,15 @@ fn setup_safe_globals(lua: &Lua) -> LuaResult<()> {
|
||||
// We do NOT include: os.exit, os.remove, os.rename, os.setlocale, os.tmpname
|
||||
// and the shell-related functions
|
||||
let os_table = lua.create_table()?;
|
||||
os_table.set("clock", lua.create_function(|_, ()| Ok(std::time::Instant::now().elapsed().as_secs_f64()))?)?;
|
||||
os_table.set(
|
||||
"clock",
|
||||
lua.create_function(|_, ()| Ok(std::time::Instant::now().elapsed().as_secs_f64()))?,
|
||||
)?;
|
||||
os_table.set("date", lua.create_function(os_date)?)?;
|
||||
os_table.set("difftime", lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?)?;
|
||||
os_table.set(
|
||||
"difftime",
|
||||
lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?,
|
||||
)?;
|
||||
os_table.set("time", lua.create_function(os_time)?)?;
|
||||
globals.set("os", os_table)?;
|
||||
|
||||
@@ -107,8 +109,7 @@ fn os_time(_lua: &Lua, _args: ()) -> LuaResult<i64> {
|
||||
|
||||
/// Load and run a Lua file in the given runtime
|
||||
pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(mlua::Error::external)?;
|
||||
let content = std::fs::read_to_string(path).map_err(mlua::Error::external)?;
|
||||
lua.load(&content)
|
||||
.set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk"))
|
||||
.into_function()?
|
||||
|
||||
@@ -59,7 +59,11 @@ pub struct ScriptRuntimeVTable {
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
|
||||
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<ScriptProviderInfo>,
|
||||
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(
|
||||
handle: RuntimeHandle,
|
||||
provider_id: RStr<'_>,
|
||||
query: RStr<'_>,
|
||||
) -> RVec<PluginItem>,
|
||||
pub drop: extern "C" fn(handle: RuntimeHandle),
|
||||
}
|
||||
|
||||
@@ -100,9 +104,8 @@ impl LoadedRuntime {
|
||||
}
|
||||
|
||||
// SAFETY: We trust the runtime library to be correct
|
||||
let library = unsafe { Library::new(library_path) }.map_err(|e| {
|
||||
PluginError::LoadError(format!("{}: {}", library_path.display(), e))
|
||||
})?;
|
||||
let library = unsafe { Library::new(library_path) }
|
||||
.map_err(|e| PluginError::LoadError(format!("{}: {}", library_path.display(), e)))?;
|
||||
|
||||
let library = Arc::new(library);
|
||||
|
||||
@@ -152,12 +155,8 @@ impl LoadedRuntime {
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|info| {
|
||||
let provider = RuntimeProvider::new(
|
||||
self.name,
|
||||
self.vtable,
|
||||
self.handle,
|
||||
info.clone(),
|
||||
);
|
||||
let provider =
|
||||
RuntimeProvider::new(self.name, self.vtable, self.handle, info.clone());
|
||||
Box::new(provider) as Box<dyn Provider>
|
||||
})
|
||||
.collect()
|
||||
@@ -227,7 +226,10 @@ impl Provider for RuntimeProvider {
|
||||
|
||||
let name_rstr = RStr::from_str(self.info.name.as_str());
|
||||
let items_rvec = (self.vtable.refresh)(self.handle, name_rstr);
|
||||
self.items = items_rvec.into_iter().map(|i| self.convert_item(i)).collect();
|
||||
self.items = items_rvec
|
||||
.into_iter()
|
||||
.map(|i| self.convert_item(i))
|
||||
.collect();
|
||||
|
||||
log::debug!(
|
||||
"[RuntimeProvider] '{}' refreshed with {} items",
|
||||
@@ -246,12 +248,16 @@ unsafe impl Send for RuntimeProvider {}
|
||||
|
||||
/// Check if the Lua runtime is available
|
||||
pub fn lua_runtime_available() -> bool {
|
||||
PathBuf::from(SYSTEM_RUNTIMES_DIR).join("liblua.so").exists()
|
||||
PathBuf::from(SYSTEM_RUNTIMES_DIR)
|
||||
.join("liblua.so")
|
||||
.exists()
|
||||
}
|
||||
|
||||
/// Check if the Rune runtime is available
|
||||
pub fn rune_runtime_available() -> bool {
|
||||
PathBuf::from(SYSTEM_RUNTIMES_DIR).join("librune.so").exists()
|
||||
PathBuf::from(SYSTEM_RUNTIMES_DIR)
|
||||
.join("librune.so")
|
||||
.exists()
|
||||
}
|
||||
|
||||
impl LoadedRuntime {
|
||||
|
||||
@@ -66,13 +66,14 @@ fn clean_desktop_exec_field(cmd: &str) -> String {
|
||||
cleaned
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ApplicationProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
|
||||
impl ApplicationProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn get_application_dirs() -> Vec<std::path::PathBuf> {
|
||||
@@ -139,15 +140,18 @@ impl Provider for ApplicationProvider {
|
||||
if !current_desktops.is_empty() {
|
||||
// OnlyShowIn: if set, current desktop must be in the list
|
||||
if desktop_entry.only_show_in().is_some_and(|only| {
|
||||
!current_desktops.iter().any(|de| only.contains(&de.as_str()))
|
||||
!current_desktops
|
||||
.iter()
|
||||
.any(|de| only.contains(&de.as_str()))
|
||||
}) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// NotShowIn: if current desktop is in the list, skip
|
||||
if desktop_entry.not_show_in().is_some_and(|not| {
|
||||
current_desktops.iter().any(|de| not.contains(&de.as_str()))
|
||||
}) {
|
||||
if desktop_entry
|
||||
.not_show_in()
|
||||
.is_some_and(|not| current_desktops.iter().any(|de| not.contains(&de.as_str())))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -197,7 +201,8 @@ impl Provider for ApplicationProvider {
|
||||
);
|
||||
|
||||
// Sort alphabetically by name
|
||||
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
self.items
|
||||
.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
@@ -219,7 +224,10 @@ mod tests {
|
||||
#[test]
|
||||
fn test_clean_desktop_exec_multiple_placeholders() {
|
||||
assert_eq!(clean_desktop_exec_field("app %f %u %U"), "app");
|
||||
assert_eq!(clean_desktop_exec_field("app --flag %u --other"), "app --flag --other");
|
||||
assert_eq!(
|
||||
clean_desktop_exec_field("app --flag %u --other"),
|
||||
"app --flag --other"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -4,13 +4,14 @@ use std::collections::HashSet;
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CommandProvider {
|
||||
items: Vec<LaunchItem>,
|
||||
}
|
||||
|
||||
impl CommandProvider {
|
||||
pub fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
Self::default()
|
||||
}
|
||||
|
||||
fn get_path_dirs() -> Vec<PathBuf> {
|
||||
@@ -97,7 +98,8 @@ impl Provider for CommandProvider {
|
||||
debug!("Found {} commands in PATH", self.items.len());
|
||||
|
||||
// Sort alphabetically
|
||||
self.items.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
self.items
|
||||
.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
}
|
||||
|
||||
fn items(&self) -> &[LaunchItem] {
|
||||
|
||||
@@ -95,9 +95,7 @@ impl Provider for LuaProvider {
|
||||
unsafe impl Send for LuaProvider {}
|
||||
|
||||
/// Create LuaProviders from all registered providers in a plugin
|
||||
pub fn create_providers_from_plugin(
|
||||
plugin: Rc<RefCell<LoadedPlugin>>,
|
||||
) -> Vec<Box<dyn Provider>> {
|
||||
pub fn create_providers_from_plugin(plugin: Rc<RefCell<LoadedPlugin>>) -> Vec<Box<dyn Provider>> {
|
||||
let registrations = {
|
||||
let p = plugin.borrow();
|
||||
match p.get_provider_registrations() {
|
||||
|
||||
@@ -141,13 +141,25 @@ impl ProviderManager {
|
||||
let type_id = provider.type_id();
|
||||
|
||||
if provider.is_dynamic() {
|
||||
info!("Registered dynamic provider: {} ({})", provider.name(), type_id);
|
||||
info!(
|
||||
"Registered dynamic provider: {} ({})",
|
||||
provider.name(),
|
||||
type_id
|
||||
);
|
||||
manager.dynamic_providers.push(provider);
|
||||
} else if provider.is_widget() {
|
||||
info!("Registered widget provider: {} ({})", provider.name(), type_id);
|
||||
info!(
|
||||
"Registered widget provider: {} ({})",
|
||||
provider.name(),
|
||||
type_id
|
||||
);
|
||||
manager.widget_providers.push(provider);
|
||||
} else {
|
||||
info!("Registered static provider: {} ({})", provider.name(), type_id);
|
||||
info!(
|
||||
"Registered static provider: {} ({})",
|
||||
provider.name(),
|
||||
type_id
|
||||
);
|
||||
manager.static_native_providers.push(provider);
|
||||
}
|
||||
}
|
||||
@@ -263,15 +275,25 @@ impl ProviderManager {
|
||||
/// Searches in all native provider lists (static, dynamic, widget)
|
||||
pub fn find_native_provider(&self, type_id: &str) -> Option<&NativeProvider> {
|
||||
// Check static native providers first (clipboard, emoji, ssh, systemd, etc.)
|
||||
if let Some(p) = self.static_native_providers.iter().find(|p| p.type_id() == type_id) {
|
||||
if let Some(p) = self
|
||||
.static_native_providers
|
||||
.iter()
|
||||
.find(|p| p.type_id() == type_id)
|
||||
{
|
||||
return Some(p);
|
||||
}
|
||||
// Check widget providers (pomodoro, weather, media)
|
||||
if let Some(p) = self.widget_providers.iter().find(|p| p.type_id() == type_id) {
|
||||
if let Some(p) = self
|
||||
.widget_providers
|
||||
.iter()
|
||||
.find(|p| p.type_id() == type_id)
|
||||
{
|
||||
return Some(p);
|
||||
}
|
||||
// Then dynamic providers (calc, websearch, filesearch)
|
||||
self.dynamic_providers.iter().find(|p| p.type_id() == type_id)
|
||||
self.dynamic_providers
|
||||
.iter()
|
||||
.find(|p| p.type_id() == type_id)
|
||||
}
|
||||
|
||||
/// Execute a plugin action command
|
||||
@@ -311,27 +333,31 @@ impl ProviderManager {
|
||||
|
||||
/// Iterate over all static provider items (core + native static plugins)
|
||||
fn all_static_items(&self) -> impl Iterator<Item = &LaunchItem> {
|
||||
self.providers
|
||||
.iter()
|
||||
.flat_map(|p| p.items().iter())
|
||||
.chain(self.static_native_providers.iter().flat_map(|p| p.items().iter()))
|
||||
self.providers.iter().flat_map(|p| p.items().iter()).chain(
|
||||
self.static_native_providers
|
||||
.iter()
|
||||
.flat_map(|p| p.items().iter()),
|
||||
)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn search(&self, query: &str, max_results: usize) -> Vec<(LaunchItem, i64)> {
|
||||
if query.is_empty() {
|
||||
// Return recent/popular items when query is empty
|
||||
return self.all_static_items()
|
||||
return self
|
||||
.all_static_items()
|
||||
.take(max_results)
|
||||
.map(|item| (item.clone(), 0))
|
||||
.collect();
|
||||
}
|
||||
|
||||
let mut results: Vec<(LaunchItem, i64)> = self.all_static_items()
|
||||
let mut results: Vec<(LaunchItem, i64)> = self
|
||||
.all_static_items()
|
||||
.filter_map(|item| {
|
||||
// Match against name and description
|
||||
let name_score = self.matcher.fuzzy_match(&item.name, query);
|
||||
let desc_score = item.description
|
||||
let desc_score = item
|
||||
.description
|
||||
.as_ref()
|
||||
.and_then(|d| self.matcher.fuzzy_match(d, query));
|
||||
|
||||
@@ -417,7 +443,10 @@ impl ProviderManager {
|
||||
tag_filter: Option<&str>,
|
||||
) -> Vec<(LaunchItem, i64)> {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Search] query={:?}, max={}, frecency_weight={}", query, max_results, frecency_weight);
|
||||
debug!(
|
||||
"[Search] query={:?}, max={}, frecency_weight={}",
|
||||
query, max_results, frecency_weight
|
||||
);
|
||||
|
||||
let mut results: Vec<(LaunchItem, i64)> = Vec::new();
|
||||
|
||||
@@ -567,7 +596,13 @@ impl ProviderManager {
|
||||
{
|
||||
debug!("[Search] Returning {} results", results.len());
|
||||
for (i, (item, score)) in results.iter().take(5).enumerate() {
|
||||
debug!("[Search] #{}: {} (score={}, provider={:?})", i + 1, item.name, score, item.provider);
|
||||
debug!(
|
||||
"[Search] #{}: {} (score={}, provider={:?})",
|
||||
i + 1,
|
||||
item.name,
|
||||
score,
|
||||
item.provider
|
||||
);
|
||||
}
|
||||
if results.len() > 5 {
|
||||
debug!("[Search] ... and {} more", results.len() - 5);
|
||||
@@ -583,7 +618,11 @@ impl ProviderManager {
|
||||
self.providers
|
||||
.iter()
|
||||
.map(|p| p.provider_type())
|
||||
.chain(self.static_native_providers.iter().map(|p| p.provider_type()))
|
||||
.chain(
|
||||
self.static_native_providers
|
||||
.iter()
|
||||
.map(|p| p.provider_type()),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -606,16 +645,10 @@ impl ProviderManager {
|
||||
Some(":cmd".to_string()),
|
||||
"utilities-terminal".to_string(),
|
||||
),
|
||||
ProviderType::Dmenu => (
|
||||
"dmenu".to_string(),
|
||||
None,
|
||||
"view-list-symbolic".to_string(),
|
||||
),
|
||||
ProviderType::Plugin(type_id) => (
|
||||
type_id,
|
||||
None,
|
||||
"application-x-addon".to_string(),
|
||||
),
|
||||
ProviderType::Dmenu => {
|
||||
("dmenu".to_string(), None, "view-list-symbolic".to_string())
|
||||
}
|
||||
ProviderType::Plugin(type_id) => (type_id, None, "application-x-addon".to_string()),
|
||||
};
|
||||
descs.push(ProviderDescriptor {
|
||||
id,
|
||||
@@ -771,7 +804,10 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Submenu] No submenu actions found for plugin '{}'", plugin_id);
|
||||
debug!(
|
||||
"[Submenu] No submenu actions found for plugin '{}'",
|
||||
plugin_id
|
||||
);
|
||||
|
||||
None
|
||||
}
|
||||
@@ -856,9 +892,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_available_providers_dmenu() {
|
||||
let providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(MockProvider::new("dmenu", ProviderType::Dmenu)),
|
||||
];
|
||||
let providers: Vec<Box<dyn Provider>> =
|
||||
vec![Box::new(MockProvider::new("dmenu", ProviderType::Dmenu))];
|
||||
let pm = ProviderManager::new(providers, Vec::new());
|
||||
let descs = pm.available_providers();
|
||||
assert_eq!(descs.len(), 1);
|
||||
@@ -895,9 +930,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_refresh_provider_unknown_does_not_panic() {
|
||||
let providers: Vec<Box<dyn Provider>> = vec![
|
||||
Box::new(MockProvider::new("Applications", ProviderType::Application)),
|
||||
];
|
||||
let providers: Vec<Box<dyn Provider>> = vec![Box::new(MockProvider::new(
|
||||
"Applications",
|
||||
ProviderType::Application,
|
||||
))];
|
||||
let mut pm = ProviderManager::new(providers, Vec::new());
|
||||
pm.refresh_provider("nonexistent");
|
||||
// Should complete without panicking
|
||||
@@ -909,8 +945,8 @@ mod tests {
|
||||
make_item("firefox", "Firefox", ProviderType::Application),
|
||||
make_item("vim", "Vim", ProviderType::Application),
|
||||
];
|
||||
let provider = MockProvider::new("Applications", ProviderType::Application)
|
||||
.with_items(items);
|
||||
let provider =
|
||||
MockProvider::new("Applications", ProviderType::Application).with_items(items);
|
||||
let providers: Vec<Box<dyn Provider>> = vec![Box::new(provider)];
|
||||
let pm = ProviderManager::new(providers, Vec::new());
|
||||
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use log::debug;
|
||||
use owlry_plugin_api::{PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition};
|
||||
use owlry_plugin_api::{
|
||||
PluginItem as ApiPluginItem, ProviderHandle, ProviderInfo, ProviderKind, ProviderPosition,
|
||||
};
|
||||
|
||||
use super::{LaunchItem, Provider, ProviderType};
|
||||
use crate::plugins::native_loader::NativePlugin;
|
||||
@@ -76,7 +78,10 @@ impl NativeProvider {
|
||||
}
|
||||
|
||||
let api_items = self.plugin.query_provider(self.handle, query);
|
||||
api_items.into_iter().map(|item| self.convert_item(item)).collect()
|
||||
api_items
|
||||
.into_iter()
|
||||
.map(|item| self.convert_item(item))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Check if this provider has a prefix that matches the query
|
||||
|
||||
@@ -141,8 +141,14 @@ impl Server {
|
||||
|
||||
let pm_guard = pm.lock().unwrap();
|
||||
let frecency_guard = frecency.lock().unwrap();
|
||||
let results =
|
||||
pm_guard.search_with_frecency(text, max, &filter, &frecency_guard, weight, None);
|
||||
let results = pm_guard.search_with_frecency(
|
||||
text,
|
||||
max,
|
||||
&filter,
|
||||
&frecency_guard,
|
||||
weight,
|
||||
None,
|
||||
);
|
||||
|
||||
Response::Results {
|
||||
items: results
|
||||
@@ -152,7 +158,10 @@ impl Server {
|
||||
}
|
||||
}
|
||||
|
||||
Request::Launch { item_id, provider: _ } => {
|
||||
Request::Launch {
|
||||
item_id,
|
||||
provider: _,
|
||||
} => {
|
||||
let mut frecency_guard = frecency.lock().unwrap();
|
||||
frecency_guard.record_launch(item_id);
|
||||
Response::Ack
|
||||
|
||||
@@ -122,7 +122,8 @@ fn test_plugin_action_request() {
|
||||
#[test]
|
||||
fn test_terminal_field_defaults_false() {
|
||||
// terminal field should default to false when missing from JSON
|
||||
let json = r#"{"id":"test","title":"Test","description":"","icon":"","provider":"cmd","score":0}"#;
|
||||
let json =
|
||||
r#"{"id":"test","title":"Test","description":"","icon":"","provider":"cmd","score":0}"#;
|
||||
let item: ResultItem = serde_json::from_str(json).unwrap();
|
||||
assert!(!item.terminal);
|
||||
}
|
||||
|
||||
@@ -37,7 +37,11 @@ fn test_server_responds_to_providers_request() {
|
||||
match resp {
|
||||
Response::Providers { list } => {
|
||||
// The default ProviderManager always has at least Application and Command
|
||||
assert!(list.len() >= 2, "expected at least 2 providers, got {}", list.len());
|
||||
assert!(
|
||||
list.len() >= 2,
|
||||
"expected at least 2 providers, got {}",
|
||||
list.len()
|
||||
);
|
||||
let ids: Vec<&str> = list.iter().map(|p| p.id.as_str()).collect();
|
||||
assert!(ids.contains(&"app"), "missing 'app' provider");
|
||||
assert!(ids.contains(&"cmd"), "missing 'cmd' provider");
|
||||
@@ -95,7 +99,10 @@ fn test_server_handles_query_request() {
|
||||
Response::Results { items } => {
|
||||
// A nonsense query should return empty or very few results
|
||||
// (no items will fuzzy-match "nonexistent_query_xyz")
|
||||
assert!(items.len() <= 5, "expected few/no results for gibberish query");
|
||||
assert!(
|
||||
items.len() <= 5,
|
||||
"expected few/no results for gibberish query"
|
||||
);
|
||||
}
|
||||
other => panic!("expected Results response, got: {:?}", other),
|
||||
}
|
||||
@@ -172,7 +179,10 @@ fn test_server_handles_submenu_for_unknown_plugin() {
|
||||
"error should mention the plugin id"
|
||||
);
|
||||
}
|
||||
other => panic!("expected Error response for unknown plugin, got: {:?}", other),
|
||||
other => panic!(
|
||||
"expected Error response for unknown plugin, got: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
|
||||
drop(stream);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-lua"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -24,11 +24,14 @@ pub fn register_provider_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
/// Implementation of owlry.provider.register()
|
||||
fn register_provider(_lua: &Lua, config: Table) -> LuaResult<()> {
|
||||
let name: String = config.get("name")?;
|
||||
let display_name: String = config.get::<Option<String>>("display_name")?
|
||||
let display_name: String = config
|
||||
.get::<Option<String>>("display_name")?
|
||||
.unwrap_or_else(|| name.clone());
|
||||
let type_id: String = config.get::<Option<String>>("type_id")?
|
||||
let type_id: String = config
|
||||
.get::<Option<String>>("type_id")?
|
||||
.unwrap_or_else(|| name.replace('-', "_"));
|
||||
let default_icon: String = config.get::<Option<String>>("default_icon")?
|
||||
let default_icon: String = config
|
||||
.get::<Option<String>>("default_icon")?
|
||||
.unwrap_or_else(|| "application-x-addon".to_string());
|
||||
let prefix: Option<String> = config.get("prefix")?;
|
||||
|
||||
@@ -116,13 +119,14 @@ fn call_provider_function(
|
||||
// First check if there's a _providers table
|
||||
if let Ok(Value::Table(providers)) = globals.get::<Value>("_owlry_providers")
|
||||
&& let Ok(Value::Table(config)) = providers.get::<Value>(provider_name)
|
||||
&& let Ok(Value::Function(func)) = config.get::<Value>(function_name) {
|
||||
let result: Value = match query {
|
||||
Some(q) => func.call(q)?,
|
||||
None => func.call(())?,
|
||||
};
|
||||
return parse_items_result(result);
|
||||
}
|
||||
&& let Ok(Value::Function(func)) = config.get::<Value>(function_name)
|
||||
{
|
||||
let result: Value = match query {
|
||||
Some(q) => func.call(q)?,
|
||||
None => func.call(())?,
|
||||
};
|
||||
return parse_items_result(result);
|
||||
}
|
||||
|
||||
// Fall back: search through globals for functions
|
||||
// This is less reliable but handles simple cases
|
||||
@@ -153,7 +157,9 @@ fn parse_item(table: &Table) -> LuaResult<PluginItem> {
|
||||
let description: Option<String> = table.get("description")?;
|
||||
let icon: Option<String> = table.get("icon")?;
|
||||
let terminal: bool = table.get::<Option<bool>>("terminal")?.unwrap_or(false);
|
||||
let tags: Vec<String> = table.get::<Option<Vec<String>>>("tags")?.unwrap_or_default();
|
||||
let tags: Vec<String> = table
|
||||
.get::<Option<Vec<String>>>("tags")?
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut item = PluginItem::new(id, name, command);
|
||||
|
||||
@@ -176,7 +182,7 @@ fn parse_item(table: &Table) -> LuaResult<PluginItem> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime::{create_lua_runtime, SandboxConfig};
|
||||
use crate::runtime::{SandboxConfig, create_lua_runtime};
|
||||
|
||||
#[test]
|
||||
fn test_register_static_provider() {
|
||||
|
||||
@@ -11,25 +11,37 @@ use std::path::{Path, PathBuf};
|
||||
pub fn register_log_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let log = lua.create_table()?;
|
||||
|
||||
log.set("debug", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[DEBUG] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
log.set(
|
||||
"debug",
|
||||
lua.create_function(|_, msg: String| {
|
||||
eprintln!("[DEBUG] {}", msg);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
log.set("info", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[INFO] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
log.set(
|
||||
"info",
|
||||
lua.create_function(|_, msg: String| {
|
||||
eprintln!("[INFO] {}", msg);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
log.set("warn", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[WARN] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
log.set(
|
||||
"warn",
|
||||
lua.create_function(|_, msg: String| {
|
||||
eprintln!("[WARN] {}", msg);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
log.set("error", lua.create_function(|_, msg: String| {
|
||||
eprintln!("[ERROR] {}", msg);
|
||||
Ok(())
|
||||
})?)?;
|
||||
log.set(
|
||||
"error",
|
||||
lua.create_function(|_, msg: String| {
|
||||
eprintln!("[ERROR] {}", msg);
|
||||
Ok(())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
owlry.set("log", log)?;
|
||||
Ok(())
|
||||
@@ -44,59 +56,79 @@ pub fn register_path_api(lua: &Lua, owlry: &Table, plugin_dir: &Path) -> LuaResu
|
||||
let path = lua.create_table()?;
|
||||
|
||||
// owlry.path.config() -> ~/.config/owlry
|
||||
path.set("config", lua.create_function(|_, ()| {
|
||||
Ok(dirs::config_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
path.set(
|
||||
"config",
|
||||
lua.create_function(|_, ()| {
|
||||
Ok(dirs::config_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.path.data() -> ~/.local/share/owlry
|
||||
path.set("data", lua.create_function(|_, ()| {
|
||||
Ok(dirs::data_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
path.set(
|
||||
"data",
|
||||
lua.create_function(|_, ()| {
|
||||
Ok(dirs::data_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.path.cache() -> ~/.cache/owlry
|
||||
path.set("cache", lua.create_function(|_, ()| {
|
||||
Ok(dirs::cache_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
path.set(
|
||||
"cache",
|
||||
lua.create_function(|_, ()| {
|
||||
Ok(dirs::cache_dir()
|
||||
.map(|d| d.join("owlry"))
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.path.home() -> ~
|
||||
path.set("home", lua.create_function(|_, ()| {
|
||||
Ok(dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?)?;
|
||||
path.set(
|
||||
"home",
|
||||
lua.create_function(|_, ()| {
|
||||
Ok(dirs::home_dir()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_default())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.path.join(...) -> joined path
|
||||
path.set("join", lua.create_function(|_, parts: mlua::Variadic<String>| {
|
||||
let mut path = PathBuf::new();
|
||||
for part in parts {
|
||||
path.push(part);
|
||||
}
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
})?)?;
|
||||
path.set(
|
||||
"join",
|
||||
lua.create_function(|_, parts: mlua::Variadic<String>| {
|
||||
let mut path = PathBuf::new();
|
||||
for part in parts {
|
||||
path.push(part);
|
||||
}
|
||||
Ok(path.to_string_lossy().to_string())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.path.plugin_dir() -> plugin directory
|
||||
let plugin_dir_str = plugin_dir.to_string_lossy().to_string();
|
||||
path.set("plugin_dir", lua.create_function(move |_, ()| {
|
||||
Ok(plugin_dir_str.clone())
|
||||
})?)?;
|
||||
path.set(
|
||||
"plugin_dir",
|
||||
lua.create_function(move |_, ()| Ok(plugin_dir_str.clone()))?,
|
||||
)?;
|
||||
|
||||
// owlry.path.expand(path) -> expanded path (~ -> home)
|
||||
path.set("expand", lua.create_function(|_, path: String| {
|
||||
if path.starts_with("~/")
|
||||
&& let Some(home) = dirs::home_dir() {
|
||||
path.set(
|
||||
"expand",
|
||||
lua.create_function(|_, path: String| {
|
||||
if path.starts_with("~/")
|
||||
&& let Some(home) = dirs::home_dir()
|
||||
{
|
||||
return Ok(home.join(&path[2..]).to_string_lossy().to_string());
|
||||
}
|
||||
Ok(path)
|
||||
})?)?;
|
||||
Ok(path)
|
||||
})?,
|
||||
)?;
|
||||
|
||||
owlry.set("path", path)?;
|
||||
Ok(())
|
||||
@@ -111,76 +143,95 @@ pub fn register_fs_api(lua: &Lua, owlry: &Table, _plugin_dir: &Path) -> LuaResul
|
||||
let fs = lua.create_table()?;
|
||||
|
||||
// owlry.fs.exists(path) -> bool
|
||||
fs.set("exists", lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
Ok(Path::new(&path).exists())
|
||||
})?)?;
|
||||
fs.set(
|
||||
"exists",
|
||||
lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
Ok(Path::new(&path).exists())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.fs.is_dir(path) -> bool
|
||||
fs.set("is_dir", lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
Ok(Path::new(&path).is_dir())
|
||||
})?)?;
|
||||
fs.set(
|
||||
"is_dir",
|
||||
lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
Ok(Path::new(&path).is_dir())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.fs.read(path) -> string or nil
|
||||
fs.set("read", lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?)?;
|
||||
fs.set(
|
||||
"read",
|
||||
lua.create_function(|_, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => Ok(Some(content)),
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.fs.read_lines(path) -> table of strings or nil
|
||||
fs.set("read_lines", lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
|
||||
Ok(Some(lua.create_sequence_from(lines)?))
|
||||
fs.set(
|
||||
"read_lines",
|
||||
lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
let lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
|
||||
Ok(Some(lua.create_sequence_from(lines)?))
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?)?;
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.fs.list_dir(path) -> table of filenames or nil
|
||||
fs.set("list_dir", lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_dir(&path) {
|
||||
Ok(entries) => {
|
||||
let names: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
Ok(Some(lua.create_sequence_from(names)?))
|
||||
fs.set(
|
||||
"list_dir",
|
||||
lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_dir(&path) {
|
||||
Ok(entries) => {
|
||||
let names: Vec<String> = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.filter_map(|e| e.file_name().into_string().ok())
|
||||
.collect();
|
||||
Ok(Some(lua.create_sequence_from(names)?))
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
Err(_) => Ok(None),
|
||||
}
|
||||
})?)?;
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.fs.read_json(path) -> table or nil
|
||||
fs.set("read_json", lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => {
|
||||
match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
fs.set(
|
||||
"read_json",
|
||||
lua.create_function(|lua, path: String| {
|
||||
let path = expand_path(&path);
|
||||
match std::fs::read_to_string(&path) {
|
||||
Ok(content) => match serde_json::from_str::<serde_json::Value>(&content) {
|
||||
Ok(value) => json_to_lua(lua, &value),
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
},
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
})?)?;
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.fs.write(path, content) -> bool
|
||||
fs.set("write", lua.create_function(|_, (path, content): (String, String)| {
|
||||
let path = expand_path(&path);
|
||||
// Create parent directories if needed
|
||||
if let Some(parent) = Path::new(&path).parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
Ok(std::fs::write(&path, content).is_ok())
|
||||
})?)?;
|
||||
fs.set(
|
||||
"write",
|
||||
lua.create_function(|_, (path, content): (String, String)| {
|
||||
let path = expand_path(&path);
|
||||
// Create parent directories if needed
|
||||
if let Some(parent) = Path::new(&path).parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
Ok(std::fs::write(&path, content).is_ok())
|
||||
})?,
|
||||
)?;
|
||||
|
||||
owlry.set("fs", fs)?;
|
||||
Ok(())
|
||||
@@ -195,18 +246,24 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
let json = lua.create_table()?;
|
||||
|
||||
// owlry.json.encode(value) -> string
|
||||
json.set("encode", lua.create_function(|lua, value: Value| {
|
||||
let json_value = lua_to_json(lua, &value)?;
|
||||
Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string()))
|
||||
})?)?;
|
||||
json.set(
|
||||
"encode",
|
||||
lua.create_function(|lua, value: Value| {
|
||||
let json_value = lua_to_json(lua, &value)?;
|
||||
Ok(serde_json::to_string(&json_value).unwrap_or_else(|_| "null".to_string()))
|
||||
})?,
|
||||
)?;
|
||||
|
||||
// owlry.json.decode(string) -> value or nil
|
||||
json.set("decode", lua.create_function(|lua, s: String| {
|
||||
match serde_json::from_str::<serde_json::Value>(&s) {
|
||||
Ok(value) => json_to_lua(lua, &value),
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
})?)?;
|
||||
json.set(
|
||||
"decode",
|
||||
lua.create_function(|lua, s: String| {
|
||||
match serde_json::from_str::<serde_json::Value>(&s) {
|
||||
Ok(value) => json_to_lua(lua, &value),
|
||||
Err(_) => Ok(Value::Nil),
|
||||
}
|
||||
})?,
|
||||
)?;
|
||||
|
||||
owlry.set("json", json)?;
|
||||
Ok(())
|
||||
@@ -219,9 +276,10 @@ pub fn register_json_api(lua: &Lua, owlry: &Table) -> LuaResult<()> {
|
||||
/// Expand ~ in paths
|
||||
fn expand_path(path: &str) -> String {
|
||||
if path.starts_with("~/")
|
||||
&& let Some(home) = dirs::home_dir() {
|
||||
return home.join(&path[2..]).to_string_lossy().to_string();
|
||||
}
|
||||
&& let Some(home) = dirs::home_dir()
|
||||
{
|
||||
return home.join(&path[2..]).to_string_lossy().to_string();
|
||||
}
|
||||
path.to_string()
|
||||
}
|
||||
|
||||
@@ -305,7 +363,7 @@ fn lua_to_json(_lua: &Lua, value: &Value) -> LuaResult<serde_json::Value> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime::{create_lua_runtime, SandboxConfig};
|
||||
use crate::runtime::{SandboxConfig, create_lua_runtime};
|
||||
|
||||
#[test]
|
||||
fn test_log_api() {
|
||||
@@ -316,7 +374,10 @@ mod tests {
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
// Just verify it doesn't panic
|
||||
lua.load("owlry.log.info('test message')").set_name("test").call::<()>(()).unwrap();
|
||||
lua.load("owlry.log.info('test message')")
|
||||
.set_name("test")
|
||||
.call::<()>(())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -327,10 +388,18 @@ mod tests {
|
||||
register_path_api(&lua, &owlry, Path::new("/tmp/test-plugin")).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let home: String = lua.load("return owlry.path.home()").set_name("test").call(()).unwrap();
|
||||
let home: String = lua
|
||||
.load("return owlry.path.home()")
|
||||
.set_name("test")
|
||||
.call(())
|
||||
.unwrap();
|
||||
assert!(!home.is_empty());
|
||||
|
||||
let plugin_dir: String = lua.load("return owlry.path.plugin_dir()").set_name("test").call(()).unwrap();
|
||||
let plugin_dir: String = lua
|
||||
.load("return owlry.path.plugin_dir()")
|
||||
.set_name("test")
|
||||
.call(())
|
||||
.unwrap();
|
||||
assert_eq!(plugin_dir, "/tmp/test-plugin");
|
||||
}
|
||||
|
||||
@@ -342,10 +411,18 @@ mod tests {
|
||||
register_fs_api(&lua, &owlry, Path::new("/tmp")).unwrap();
|
||||
lua.globals().set("owlry", owlry).unwrap();
|
||||
|
||||
let exists: bool = lua.load("return owlry.fs.exists('/tmp')").set_name("test").call(()).unwrap();
|
||||
let exists: bool = lua
|
||||
.load("return owlry.fs.exists('/tmp')")
|
||||
.set_name("test")
|
||||
.call(())
|
||||
.unwrap();
|
||||
assert!(exists);
|
||||
|
||||
let is_dir: bool = lua.load("return owlry.fs.is_dir('/tmp')").set_name("test").call(()).unwrap();
|
||||
let is_dir: bool = lua
|
||||
.load("return owlry.fs.is_dir('/tmp')")
|
||||
.set_name("test")
|
||||
.call(())
|
||||
.unwrap();
|
||||
assert!(is_dir);
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,11 @@ pub struct LuaRuntimeVTable {
|
||||
/// Refresh a provider's items
|
||||
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
|
||||
/// Query a dynamic provider
|
||||
pub query: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(
|
||||
handle: RuntimeHandle,
|
||||
provider_id: RStr<'_>,
|
||||
query: RStr<'_>,
|
||||
) -> RVec<PluginItem>,
|
||||
/// Cleanup and drop the runtime
|
||||
pub drop: extern "C" fn(handle: RuntimeHandle),
|
||||
}
|
||||
@@ -83,11 +87,15 @@ impl RuntimeHandle {
|
||||
/// Create a null handle (reserved for error cases)
|
||||
#[allow(dead_code)]
|
||||
fn null() -> Self {
|
||||
Self { ptr: std::ptr::null_mut() }
|
||||
Self {
|
||||
ptr: std::ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
fn from_box<T>(state: Box<T>) -> Self {
|
||||
Self { ptr: Box::into_raw(state) as *mut () }
|
||||
Self {
|
||||
ptr: Box::into_raw(state) as *mut (),
|
||||
}
|
||||
}
|
||||
|
||||
unsafe fn drop_as<T>(&self) {
|
||||
@@ -147,7 +155,10 @@ impl LuaRuntimeState {
|
||||
for (id, (manifest, path)) in discovered {
|
||||
// Check version compatibility
|
||||
if !manifest.is_compatible_with(owlry_version) {
|
||||
eprintln!("owlry-lua: Plugin '{}' not compatible with owlry {}", id, owlry_version);
|
||||
eprintln!(
|
||||
"owlry-lua: Plugin '{}' not compatible with owlry {}",
|
||||
id, owlry_version
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -285,13 +296,19 @@ extern "C" fn runtime_refresh(handle: RuntimeHandle, provider_id: RStr<'_>) -> R
|
||||
state.refresh_provider(provider_id.as_str()).into()
|
||||
}
|
||||
|
||||
extern "C" fn runtime_query(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem> {
|
||||
extern "C" fn runtime_query(
|
||||
handle: RuntimeHandle,
|
||||
provider_id: RStr<'_>,
|
||||
query: RStr<'_>,
|
||||
) -> RVec<PluginItem> {
|
||||
if handle.ptr.is_null() {
|
||||
return RVec::new();
|
||||
}
|
||||
|
||||
let state = unsafe { &*(handle.ptr as *const LuaRuntimeState) };
|
||||
state.query_provider(provider_id.as_str(), query.as_str()).into()
|
||||
state
|
||||
.query_provider(provider_id.as_str(), query.as_str())
|
||||
.into()
|
||||
}
|
||||
|
||||
extern "C" fn runtime_drop(handle: RuntimeHandle) {
|
||||
|
||||
@@ -8,7 +8,7 @@ use owlry_plugin_api::PluginItem;
|
||||
|
||||
use crate::api;
|
||||
use crate::manifest::PluginManifest;
|
||||
use crate::runtime::{create_lua_runtime, load_file, SandboxConfig};
|
||||
use crate::runtime::{SandboxConfig, create_lua_runtime, load_file};
|
||||
|
||||
/// Provider registration info from Lua
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -77,11 +77,13 @@ impl LoadedPlugin {
|
||||
// Load the entry point file
|
||||
let entry_path = self.path.join(&self.manifest.plugin.entry);
|
||||
if !entry_path.exists() {
|
||||
return Err(format!("Entry point '{}' not found", self.manifest.plugin.entry));
|
||||
return Err(format!(
|
||||
"Entry point '{}' not found",
|
||||
self.manifest.plugin.entry
|
||||
));
|
||||
}
|
||||
|
||||
load_file(&lua, &entry_path)
|
||||
.map_err(|e| format!("Failed to load entry point: {}", e))?;
|
||||
load_file(&lua, &entry_path).map_err(|e| format!("Failed to load entry point: {}", e))?;
|
||||
|
||||
self.lua = Some(lua);
|
||||
Ok(())
|
||||
@@ -89,7 +91,9 @@ impl LoadedPlugin {
|
||||
|
||||
/// Get provider registrations from this plugin
|
||||
pub fn get_provider_registrations(&self) -> Result<Vec<ProviderRegistration>, String> {
|
||||
let lua = self.lua.as_ref()
|
||||
let lua = self
|
||||
.lua
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Plugin not initialized".to_string())?;
|
||||
|
||||
api::get_provider_registrations(lua)
|
||||
@@ -98,25 +102,33 @@ impl LoadedPlugin {
|
||||
|
||||
/// Call a provider's refresh function
|
||||
pub fn call_provider_refresh(&self, provider_name: &str) -> Result<Vec<PluginItem>, String> {
|
||||
let lua = self.lua.as_ref()
|
||||
let lua = self
|
||||
.lua
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Plugin not initialized".to_string())?;
|
||||
|
||||
api::call_refresh(lua, provider_name)
|
||||
.map_err(|e| format!("Refresh failed: {}", e))
|
||||
api::call_refresh(lua, provider_name).map_err(|e| format!("Refresh failed: {}", e))
|
||||
}
|
||||
|
||||
/// Call a provider's query function
|
||||
pub fn call_provider_query(&self, provider_name: &str, query: &str) -> Result<Vec<PluginItem>, String> {
|
||||
let lua = self.lua.as_ref()
|
||||
pub fn call_provider_query(
|
||||
&self,
|
||||
provider_name: &str,
|
||||
query: &str,
|
||||
) -> Result<Vec<PluginItem>, String> {
|
||||
let lua = self
|
||||
.lua
|
||||
.as_ref()
|
||||
.ok_or_else(|| "Plugin not initialized".to_string())?;
|
||||
|
||||
api::call_query(lua, provider_name, query)
|
||||
.map_err(|e| format!("Query failed: {}", e))
|
||||
api::call_query(lua, provider_name, query).map_err(|e| format!("Query failed: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
/// Discover plugins in a directory
|
||||
pub fn discover_plugins(plugins_dir: &Path) -> Result<HashMap<String, (PluginManifest, PathBuf)>, String> {
|
||||
pub fn discover_plugins(
|
||||
plugins_dir: &Path,
|
||||
) -> Result<HashMap<String, (PluginManifest, PathBuf)>, String> {
|
||||
let mut plugins = HashMap::new();
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
@@ -146,13 +158,21 @@ pub fn discover_plugins(plugins_dir: &Path) -> Result<HashMap<String, (PluginMan
|
||||
Ok(manifest) => {
|
||||
let id = manifest.plugin.id.clone();
|
||||
if plugins.contains_key(&id) {
|
||||
eprintln!("owlry-lua: Duplicate plugin ID '{}', skipping {}", id, path.display());
|
||||
eprintln!(
|
||||
"owlry-lua: Duplicate plugin ID '{}', skipping {}",
|
||||
id,
|
||||
path.display()
|
||||
);
|
||||
continue;
|
||||
}
|
||||
plugins.insert(id, (manifest, path));
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("owlry-lua: Failed to load plugin at {}: {}", path.display(), e);
|
||||
eprintln!(
|
||||
"owlry-lua: Failed to load plugin at {}: {}",
|
||||
path.display(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,10 +90,10 @@ pub struct PluginPermissions {
|
||||
impl PluginManifest {
|
||||
/// Load a plugin manifest from a plugin.toml file
|
||||
pub fn load(path: &Path) -> Result<Self, String> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("Failed to read manifest: {}", e))?;
|
||||
let manifest: PluginManifest = toml::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
|
||||
let content =
|
||||
std::fs::read_to_string(path).map_err(|e| format!("Failed to read manifest: {}", e))?;
|
||||
let manifest: PluginManifest =
|
||||
toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
@@ -105,7 +105,12 @@ impl PluginManifest {
|
||||
return Err("Plugin ID cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if !self.plugin.id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
|
||||
if !self
|
||||
.plugin
|
||||
.id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
return Err("Plugin ID must be lowercase alphanumeric with hyphens".to_string());
|
||||
}
|
||||
|
||||
@@ -116,7 +121,10 @@ impl PluginManifest {
|
||||
|
||||
// Validate owlry_version constraint
|
||||
if semver::VersionReq::parse(&self.plugin.owlry_version).is_err() {
|
||||
return Err(format!("Invalid owlry_version constraint: {}", self.plugin.owlry_version));
|
||||
return Err(format!(
|
||||
"Invalid owlry_version constraint: {}",
|
||||
self.plugin.owlry_version
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -28,7 +28,7 @@ impl Default for SandboxConfig {
|
||||
allow_commands: false,
|
||||
allow_network: false,
|
||||
allow_external_fs: false,
|
||||
max_run_time_ms: 5000, // 5 seconds
|
||||
max_run_time_ms: 5000, // 5 seconds
|
||||
max_memory: 64 * 1024 * 1024, // 64 MB
|
||||
}
|
||||
}
|
||||
@@ -50,11 +50,7 @@ impl SandboxConfig {
|
||||
pub fn create_lua_runtime(_sandbox: &SandboxConfig) -> LuaResult<Lua> {
|
||||
// Create Lua with safe standard libraries only
|
||||
// We exclude: debug, io, os (dangerous parts), package (loadlib), ffi
|
||||
let libs = StdLib::COROUTINE
|
||||
| StdLib::TABLE
|
||||
| StdLib::STRING
|
||||
| StdLib::UTF8
|
||||
| StdLib::MATH;
|
||||
let libs = StdLib::COROUTINE | StdLib::TABLE | StdLib::STRING | StdLib::UTF8 | StdLib::MATH;
|
||||
|
||||
let lua = Lua::new_with(libs, mlua::LuaOptions::default())?;
|
||||
|
||||
@@ -74,11 +70,15 @@ fn setup_safe_globals(lua: &Lua) -> LuaResult<()> {
|
||||
|
||||
// Create a restricted os table with only safe functions
|
||||
let os_table = lua.create_table()?;
|
||||
os_table.set("clock", lua.create_function(|_, ()| {
|
||||
Ok(std::time::Instant::now().elapsed().as_secs_f64())
|
||||
})?)?;
|
||||
os_table.set(
|
||||
"clock",
|
||||
lua.create_function(|_, ()| Ok(std::time::Instant::now().elapsed().as_secs_f64()))?,
|
||||
)?;
|
||||
os_table.set("date", lua.create_function(os_date)?)?;
|
||||
os_table.set("difftime", lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?)?;
|
||||
os_table.set(
|
||||
"difftime",
|
||||
lua.create_function(|_, (t2, t1): (f64, f64)| Ok(t2 - t1))?,
|
||||
)?;
|
||||
os_table.set("time", lua.create_function(os_time)?)?;
|
||||
globals.set("os", os_table)?;
|
||||
|
||||
@@ -107,8 +107,7 @@ fn os_time(_lua: &Lua, _args: ()) -> LuaResult<i64> {
|
||||
|
||||
/// Load and run a Lua file in the given runtime
|
||||
pub fn load_file(lua: &Lua, path: &std::path::Path) -> LuaResult<()> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(mlua::Error::external)?;
|
||||
let content = std::fs::read_to_string(path).map_err(mlua::Error::external)?;
|
||||
lua.load(&content)
|
||||
.set_name(path.file_name().and_then(|n| n.to_str()).unwrap_or("chunk"))
|
||||
.into_function()?
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-api"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -284,12 +284,8 @@ pub enum NotifyUrgency {
|
||||
pub struct HostAPI {
|
||||
/// Send a notification to the user
|
||||
/// Parameters: summary, body, icon (optional, empty string for none), urgency
|
||||
pub notify: extern "C" fn(
|
||||
summary: RStr<'_>,
|
||||
body: RStr<'_>,
|
||||
icon: RStr<'_>,
|
||||
urgency: NotifyUrgency,
|
||||
),
|
||||
pub notify:
|
||||
extern "C" fn(summary: RStr<'_>, body: RStr<'_>, icon: RStr<'_>, urgency: NotifyUrgency),
|
||||
|
||||
/// Log a message at info level
|
||||
pub log_info: extern "C" fn(message: RStr<'_>),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-rune"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "Rune scripting runtime for owlry plugins"
|
||||
|
||||
@@ -75,7 +75,11 @@ pub struct RuneRuntimeVTable {
|
||||
pub init: extern "C" fn(plugins_dir: RStr<'_>) -> RuntimeHandle,
|
||||
pub providers: extern "C" fn(handle: RuntimeHandle) -> RVec<RuneProviderInfo>,
|
||||
pub refresh: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(handle: RuntimeHandle, provider_id: RStr<'_>, query: RStr<'_>) -> RVec<PluginItem>,
|
||||
pub query: extern "C" fn(
|
||||
handle: RuntimeHandle,
|
||||
provider_id: RStr<'_>,
|
||||
query: RStr<'_>,
|
||||
) -> RVec<PluginItem>,
|
||||
pub drop: extern "C" fn(handle: RuntimeHandle),
|
||||
}
|
||||
|
||||
@@ -94,7 +98,10 @@ extern "C" fn runtime_init(plugins_dir: RStr<'_>) -> RuntimeHandle {
|
||||
let _ = env_logger::try_init();
|
||||
|
||||
let plugins_dir = PathBuf::from(plugins_dir.as_str());
|
||||
log::info!("Initializing Rune runtime with plugins from: {}", plugins_dir.display());
|
||||
log::info!(
|
||||
"Initializing Rune runtime with plugins from: {}",
|
||||
plugins_dir.display()
|
||||
);
|
||||
|
||||
let mut state = RuntimeState {
|
||||
plugins: HashMap::new(),
|
||||
@@ -113,15 +120,20 @@ extern "C" fn runtime_init(plugins_dir: RStr<'_>) -> RuntimeHandle {
|
||||
type_id: RString::from(reg.type_id.as_str()),
|
||||
default_icon: RString::from(reg.default_icon.as_str()),
|
||||
is_static: reg.is_static,
|
||||
prefix: reg.prefix.as_ref()
|
||||
prefix: reg
|
||||
.prefix
|
||||
.as_ref()
|
||||
.map(|p| RString::from(p.as_str()))
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
state.plugins.insert(id, plugin);
|
||||
}
|
||||
log::info!("Loaded {} Rune plugin(s) with {} provider(s)",
|
||||
state.plugins.len(), state.providers.len());
|
||||
log::info!(
|
||||
"Loaded {} Rune plugin(s) with {} provider(s)",
|
||||
state.plugins.len(),
|
||||
state.providers.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to discover Rune plugins: {}", e);
|
||||
|
||||
@@ -8,7 +8,7 @@ use rune::{Context, Unit};
|
||||
|
||||
use crate::api::{self, ProviderRegistration};
|
||||
use crate::manifest::PluginManifest;
|
||||
use crate::runtime::{compile_source, create_context, create_vm, SandboxConfig};
|
||||
use crate::runtime::{SandboxConfig, compile_source, create_context, create_vm};
|
||||
|
||||
use owlry_plugin_api::PluginItem;
|
||||
|
||||
@@ -29,8 +29,8 @@ impl LoadedPlugin {
|
||||
/// Create and initialize a new plugin
|
||||
pub fn new(manifest: PluginManifest, path: PathBuf) -> Result<Self, String> {
|
||||
let sandbox = SandboxConfig::from_permissions(&manifest.permissions);
|
||||
let context = create_context(&sandbox)
|
||||
.map_err(|e| format!("Failed to create context: {}", e))?;
|
||||
let context =
|
||||
create_context(&sandbox).map_err(|e| format!("Failed to create context: {}", e))?;
|
||||
|
||||
let entry_path = path.join(&manifest.plugin.entry);
|
||||
if !entry_path.exists() {
|
||||
@@ -45,15 +45,14 @@ impl LoadedPlugin {
|
||||
.map_err(|e| format!("Failed to compile: {}", e))?;
|
||||
|
||||
// Run the entry point to register providers
|
||||
let mut vm = create_vm(&context, unit.clone())
|
||||
.map_err(|e| format!("Failed to create VM: {}", e))?;
|
||||
let mut vm =
|
||||
create_vm(&context, unit.clone()).map_err(|e| format!("Failed to create VM: {}", e))?;
|
||||
|
||||
// Execute the main function if it exists
|
||||
match vm.call(rune::Hash::type_hash(["main"]), ()) {
|
||||
Ok(result) => {
|
||||
// Try to complete the execution
|
||||
let _: () = rune::from_value(result)
|
||||
.unwrap_or(());
|
||||
let _: () = rune::from_value(result).unwrap_or(());
|
||||
}
|
||||
Err(_) => {
|
||||
// No main function is okay
|
||||
@@ -111,7 +110,10 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result<HashMap<String, Loade
|
||||
let mut plugins = HashMap::new();
|
||||
|
||||
if !plugins_dir.exists() {
|
||||
log::debug!("Plugins directory does not exist: {}", plugins_dir.display());
|
||||
log::debug!(
|
||||
"Plugins directory does not exist: {}",
|
||||
plugins_dir.display()
|
||||
);
|
||||
return Ok(plugins);
|
||||
}
|
||||
|
||||
@@ -135,7 +137,11 @@ pub fn discover_rune_plugins(plugins_dir: &Path) -> Result<HashMap<String, Loade
|
||||
let manifest = match PluginManifest::load(&manifest_path) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
log::warn!("Failed to load manifest at {}: {}", manifest_path.display(), e);
|
||||
log::warn!(
|
||||
"Failed to load manifest at {}: {}",
|
||||
manifest_path.display(),
|
||||
e
|
||||
);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,10 +64,10 @@ pub struct PluginPermissions {
|
||||
impl PluginManifest {
|
||||
/// Load manifest from a plugin.toml file
|
||||
pub fn load(path: &Path) -> Result<Self, String> {
|
||||
let content = std::fs::read_to_string(path)
|
||||
.map_err(|e| format!("Failed to read manifest: {}", e))?;
|
||||
let manifest: PluginManifest = toml::from_str(&content)
|
||||
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
|
||||
let content =
|
||||
std::fs::read_to_string(path).map_err(|e| format!("Failed to read manifest: {}", e))?;
|
||||
let manifest: PluginManifest =
|
||||
toml::from_str(&content).map_err(|e| format!("Failed to parse manifest: {}", e))?;
|
||||
manifest.validate()?;
|
||||
Ok(manifest)
|
||||
}
|
||||
@@ -78,7 +78,12 @@ impl PluginManifest {
|
||||
return Err("Plugin ID cannot be empty".to_string());
|
||||
}
|
||||
|
||||
if !self.plugin.id.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') {
|
||||
if !self
|
||||
.plugin
|
||||
.id
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
|
||||
{
|
||||
return Err("Plugin ID must be lowercase alphanumeric with hyphens".to_string());
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ pub struct SandboxConfig {
|
||||
pub allowed_commands: Vec<String>,
|
||||
}
|
||||
|
||||
|
||||
impl SandboxConfig {
|
||||
/// Create sandbox config from plugin permissions
|
||||
pub fn from_permissions(permissions: &PluginPermissions) -> Self {
|
||||
@@ -59,12 +58,9 @@ pub fn create_context(sandbox: &SandboxConfig) -> Result<Context, rune::ContextE
|
||||
}
|
||||
|
||||
/// Compile Rune source code into a Unit
|
||||
pub fn compile_source(
|
||||
context: &Context,
|
||||
source_path: &Path,
|
||||
) -> Result<Arc<Unit>, CompileError> {
|
||||
let source_content = std::fs::read_to_string(source_path)
|
||||
.map_err(|e| CompileError::Io(e.to_string()))?;
|
||||
pub fn compile_source(context: &Context, source_path: &Path) -> Result<Arc<Unit>, CompileError> {
|
||||
let source_content =
|
||||
std::fs::read_to_string(source_path).map_err(|e| CompileError::Io(e.to_string()))?;
|
||||
|
||||
let source_name = source_path
|
||||
.file_name()
|
||||
@@ -73,7 +69,10 @@ pub fn compile_source(
|
||||
|
||||
let mut sources = Sources::new();
|
||||
sources
|
||||
.insert(Source::new(source_name, &source_content).map_err(|e| CompileError::Compile(e.to_string()))?)
|
||||
.insert(
|
||||
Source::new(source_name, &source_content)
|
||||
.map_err(|e| CompileError::Compile(e.to_string()))?,
|
||||
)
|
||||
.map_err(|e| CompileError::Compile(format!("Failed to insert source: {}", e)))?;
|
||||
|
||||
let mut diagnostics = Diagnostics::new();
|
||||
@@ -97,13 +96,11 @@ pub fn compile_source(
|
||||
}
|
||||
|
||||
/// Create a new Rune VM from compiled unit
|
||||
pub fn create_vm(
|
||||
context: &Context,
|
||||
unit: Arc<Unit>,
|
||||
) -> Result<Vm, CompileError> {
|
||||
pub fn create_vm(context: &Context, unit: Arc<Unit>) -> Result<Vm, CompileError> {
|
||||
let runtime = Arc::new(
|
||||
context.runtime()
|
||||
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))?
|
||||
context
|
||||
.runtime()
|
||||
.map_err(|e| CompileError::Compile(format!("Failed to get runtime: {}", e)))?,
|
||||
);
|
||||
Ok(Vm::new(runtime, unit))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "0.4.10"
|
||||
version = "1.0.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||
|
||||
@@ -4,15 +4,15 @@ use crate::client::CoreClient;
|
||||
use crate::providers::DmenuProvider;
|
||||
use crate::theme;
|
||||
use crate::ui::MainWindow;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{Application, CssProvider, gio};
|
||||
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
||||
use log::{debug, info, warn};
|
||||
use owlry_core::config::Config;
|
||||
use owlry_core::data::FrecencyStore;
|
||||
use owlry_core::filter::ProviderFilter;
|
||||
use owlry_core::paths;
|
||||
use owlry_core::providers::{Provider, ProviderManager, ProviderType};
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{gio, Application, CssProvider};
|
||||
use gtk4_layer_shell::{Edge, Layer, LayerShell};
|
||||
use log::{debug, info, warn};
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
@@ -61,7 +61,7 @@ impl OwlryApp {
|
||||
let frecency = FrecencyStore::load_or_default();
|
||||
|
||||
SearchBackend::Local {
|
||||
providers: provider_manager,
|
||||
providers: Box::new(provider_manager),
|
||||
frecency,
|
||||
}
|
||||
} else {
|
||||
@@ -98,11 +98,7 @@ impl OwlryApp {
|
||||
&config.borrow().providers,
|
||||
)
|
||||
} else {
|
||||
ProviderFilter::new(
|
||||
None,
|
||||
Some(provider_types),
|
||||
&config.borrow().providers,
|
||||
)
|
||||
ProviderFilter::new(None, Some(provider_types), &config.borrow().providers)
|
||||
}
|
||||
} else {
|
||||
ProviderFilter::new(None, None, &config.borrow().providers)
|
||||
@@ -180,7 +176,7 @@ impl OwlryApp {
|
||||
let frecency = FrecencyStore::load_or_default();
|
||||
|
||||
SearchBackend::Local {
|
||||
providers: provider_manager,
|
||||
providers: Box::new(provider_manager),
|
||||
frecency,
|
||||
}
|
||||
}
|
||||
@@ -241,16 +237,17 @@ impl OwlryApp {
|
||||
|
||||
// 3. Load user's custom stylesheet if exists
|
||||
if let Some(custom_path) = paths::custom_style_file()
|
||||
&& custom_path.exists() {
|
||||
let custom_provider = CssProvider::new();
|
||||
custom_provider.load_from_path(&custom_path);
|
||||
gtk4::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&custom_provider,
|
||||
gtk4::STYLE_PROVIDER_PRIORITY_USER,
|
||||
);
|
||||
debug!("Loaded custom CSS from {:?}", custom_path);
|
||||
}
|
||||
&& custom_path.exists()
|
||||
{
|
||||
let custom_provider = CssProvider::new();
|
||||
custom_provider.load_from_path(&custom_path);
|
||||
gtk4::style_context_add_provider_for_display(
|
||||
&display,
|
||||
&custom_provider,
|
||||
gtk4::STYLE_PROVIDER_PRIORITY_USER,
|
||||
);
|
||||
debug!("Loaded custom CSS from {:?}", custom_path);
|
||||
}
|
||||
|
||||
// 4. Inject config variables (highest priority for overrides)
|
||||
let vars_css = theme::generate_variables_css(&config.appearance);
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
//! In dmenu mode, the UI uses a local ProviderManager directly (no daemon).
|
||||
|
||||
use crate::client::CoreClient;
|
||||
use log::warn;
|
||||
use owlry_core::config::Config;
|
||||
use owlry_core::data::FrecencyStore;
|
||||
use owlry_core::filter::ProviderFilter;
|
||||
use owlry_core::ipc::ResultItem;
|
||||
use owlry_core::providers::{LaunchItem, ProviderManager, ProviderType};
|
||||
use owlry_core::data::FrecencyStore;
|
||||
use owlry_core::config::Config;
|
||||
use log::warn;
|
||||
|
||||
/// Backend for search operations. Wraps either an IPC client (daemon mode)
|
||||
/// or a local ProviderManager (dmenu mode).
|
||||
@@ -18,7 +18,7 @@ pub enum SearchBackend {
|
||||
Daemon(CoreClient),
|
||||
/// Direct local provider manager (dmenu mode only)
|
||||
Local {
|
||||
providers: ProviderManager,
|
||||
providers: Box<ProviderManager>,
|
||||
frecency: FrecencyStore,
|
||||
},
|
||||
}
|
||||
@@ -64,7 +64,14 @@ impl SearchBackend {
|
||||
|
||||
if use_frecency {
|
||||
providers
|
||||
.search_with_frecency(query, max_results, filter, frecency, frecency_weight, None)
|
||||
.search_with_frecency(
|
||||
query,
|
||||
max_results,
|
||||
filter,
|
||||
frecency,
|
||||
frecency_weight,
|
||||
None,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
@@ -123,7 +130,14 @@ impl SearchBackend {
|
||||
|
||||
if use_frecency {
|
||||
providers
|
||||
.search_with_frecency(query, max_results, filter, frecency, frecency_weight, tag_filter)
|
||||
.search_with_frecency(
|
||||
query,
|
||||
max_results,
|
||||
filter,
|
||||
frecency,
|
||||
frecency_weight,
|
||||
tag_filter,
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(item, _)| item)
|
||||
.collect()
|
||||
@@ -141,18 +155,14 @@ impl SearchBackend {
|
||||
/// Execute a plugin action command. Returns true if handled.
|
||||
pub fn execute_plugin_action(&mut self, command: &str) -> bool {
|
||||
match self {
|
||||
SearchBackend::Daemon(client) => {
|
||||
match client.plugin_action(command) {
|
||||
Ok(handled) => handled,
|
||||
Err(e) => {
|
||||
warn!("IPC plugin_action failed: {}", e);
|
||||
false
|
||||
}
|
||||
SearchBackend::Daemon(client) => match client.plugin_action(command) {
|
||||
Ok(handled) => handled,
|
||||
Err(e) => {
|
||||
warn!("IPC plugin_action failed: {}", e);
|
||||
false
|
||||
}
|
||||
}
|
||||
SearchBackend::Local { providers, .. } => {
|
||||
providers.execute_plugin_action(command)
|
||||
}
|
||||
},
|
||||
SearchBackend::Local { providers, .. } => providers.execute_plugin_action(command),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,20 +175,18 @@ impl SearchBackend {
|
||||
display_name: &str,
|
||||
) -> Option<(String, Vec<LaunchItem>)> {
|
||||
match self {
|
||||
SearchBackend::Daemon(client) => {
|
||||
match client.submenu(plugin_id, data) {
|
||||
Ok(items) if !items.is_empty() => {
|
||||
let actions: Vec<LaunchItem> =
|
||||
items.into_iter().map(result_to_launch_item).collect();
|
||||
Some((display_name.to_string(), actions))
|
||||
}
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
warn!("IPC submenu query failed: {}", e);
|
||||
None
|
||||
}
|
||||
SearchBackend::Daemon(client) => match client.submenu(plugin_id, data) {
|
||||
Ok(items) if !items.is_empty() => {
|
||||
let actions: Vec<LaunchItem> =
|
||||
items.into_iter().map(result_to_launch_item).collect();
|
||||
Some((display_name.to_string(), actions))
|
||||
}
|
||||
}
|
||||
Ok(_) => None,
|
||||
Err(e) => {
|
||||
warn!("IPC submenu query failed: {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
SearchBackend::Local { providers, .. } => {
|
||||
providers.query_submenu_actions(plugin_id, data, display_name)
|
||||
}
|
||||
@@ -218,22 +226,18 @@ impl SearchBackend {
|
||||
#[allow(dead_code)]
|
||||
pub fn available_provider_ids(&mut self) -> Vec<String> {
|
||||
match self {
|
||||
SearchBackend::Daemon(client) => {
|
||||
match client.providers() {
|
||||
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
|
||||
Err(e) => {
|
||||
warn!("IPC providers query failed: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
SearchBackend::Daemon(client) => match client.providers() {
|
||||
Ok(descs) => descs.into_iter().map(|d| d.id).collect(),
|
||||
Err(e) => {
|
||||
warn!("IPC providers query failed: {}", e);
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
SearchBackend::Local { providers, .. } => {
|
||||
providers
|
||||
.available_providers()
|
||||
.into_iter()
|
||||
.map(|d| d.id)
|
||||
.collect()
|
||||
}
|
||||
},
|
||||
SearchBackend::Local { providers, .. } => providers
|
||||
.available_providers()
|
||||
.into_iter()
|
||||
.map(|d| d.id)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,20 +41,14 @@ impl CoreClient {
|
||||
.args(["--user", "start", "owlry-core"])
|
||||
.status()
|
||||
.map_err(|e| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!("failed to start owlry-core via systemd: {e}"),
|
||||
)
|
||||
io::Error::other(format!("failed to start owlry-core via systemd: {e}"))
|
||||
})?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
format!(
|
||||
"systemctl --user start owlry-core exited with status {}",
|
||||
status
|
||||
),
|
||||
));
|
||||
return Err(io::Error::other(format!(
|
||||
"systemctl --user start owlry-core exited with status {}",
|
||||
status
|
||||
)));
|
||||
}
|
||||
|
||||
// Retry with exponential backoff.
|
||||
@@ -66,9 +60,7 @@ impl CoreClient {
|
||||
Err(e) if i == delays.len() - 1 => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::ConnectionRefused,
|
||||
format!(
|
||||
"daemon started but socket not available after retries: {e}"
|
||||
),
|
||||
format!("daemon started but socket not available after retries: {e}"),
|
||||
));
|
||||
}
|
||||
Err(_) => continue,
|
||||
@@ -87,11 +79,7 @@ impl CoreClient {
|
||||
}
|
||||
|
||||
/// Send a search query and return matching results.
|
||||
pub fn query(
|
||||
&mut self,
|
||||
text: &str,
|
||||
modes: Option<Vec<String>>,
|
||||
) -> io::Result<Vec<ResultItem>> {
|
||||
pub fn query(&mut self, text: &str, modes: Option<Vec<String>>) -> io::Result<Vec<ResultItem>> {
|
||||
self.send(&Request::Query {
|
||||
text: text.to_string(),
|
||||
modes,
|
||||
@@ -99,9 +87,7 @@ impl CoreClient {
|
||||
|
||||
match self.receive()? {
|
||||
Response::Results { items } => Ok(items),
|
||||
Response::Error { message } => {
|
||||
Err(io::Error::new(io::ErrorKind::Other, message))
|
||||
}
|
||||
Response::Error { message } => Err(io::Error::other(message)),
|
||||
other => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unexpected response to Query: {other:?}"),
|
||||
@@ -118,9 +104,7 @@ impl CoreClient {
|
||||
|
||||
match self.receive()? {
|
||||
Response::Ack => Ok(()),
|
||||
Response::Error { message } => {
|
||||
Err(io::Error::new(io::ErrorKind::Other, message))
|
||||
}
|
||||
Response::Error { message } => Err(io::Error::other(message)),
|
||||
other => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unexpected response to Launch: {other:?}"),
|
||||
@@ -134,9 +118,7 @@ impl CoreClient {
|
||||
|
||||
match self.receive()? {
|
||||
Response::Providers { list } => Ok(list),
|
||||
Response::Error { message } => {
|
||||
Err(io::Error::new(io::ErrorKind::Other, message))
|
||||
}
|
||||
Response::Error { message } => Err(io::Error::other(message)),
|
||||
other => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unexpected response to Providers: {other:?}"),
|
||||
@@ -150,9 +132,7 @@ impl CoreClient {
|
||||
|
||||
match self.receive()? {
|
||||
Response::Ack => Ok(()),
|
||||
Response::Error { message } => {
|
||||
Err(io::Error::new(io::ErrorKind::Other, message))
|
||||
}
|
||||
Response::Error { message } => Err(io::Error::other(message)),
|
||||
other => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unexpected response to Toggle: {other:?}"),
|
||||
@@ -178,11 +158,7 @@ impl CoreClient {
|
||||
}
|
||||
|
||||
/// Query a plugin's submenu actions.
|
||||
pub fn submenu(
|
||||
&mut self,
|
||||
plugin_id: &str,
|
||||
data: &str,
|
||||
) -> io::Result<Vec<ResultItem>> {
|
||||
pub fn submenu(&mut self, plugin_id: &str, data: &str) -> io::Result<Vec<ResultItem>> {
|
||||
self.send(&Request::Submenu {
|
||||
plugin_id: plugin_id.to_string(),
|
||||
data: data.to_string(),
|
||||
@@ -190,9 +166,7 @@ impl CoreClient {
|
||||
|
||||
match self.receive()? {
|
||||
Response::SubmenuItems { items } => Ok(items),
|
||||
Response::Error { message } => {
|
||||
Err(io::Error::new(io::ErrorKind::Other, message))
|
||||
}
|
||||
Response::Error { message } => Err(io::Error::other(message)),
|
||||
other => Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
format!("unexpected response to Submenu: {other:?}"),
|
||||
@@ -220,8 +194,7 @@ impl CoreClient {
|
||||
"daemon closed the connection",
|
||||
));
|
||||
}
|
||||
serde_json::from_str(line.trim())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
serde_json::from_str(line.trim()).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -239,11 +212,7 @@ mod tests {
|
||||
/// socket path to avoid collisions when tests run in parallel.
|
||||
fn mock_server(response: Response) -> PathBuf {
|
||||
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"owlry-test-{}-{}",
|
||||
std::process::id(),
|
||||
n
|
||||
));
|
||||
let dir = std::env::temp_dir().join(format!("owlry-test-{}-{}", std::process::id(), n));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
let sock = dir.join("test.sock");
|
||||
let _ = std::fs::remove_file(&sock);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
mod app;
|
||||
mod backend;
|
||||
pub mod client;
|
||||
mod cli;
|
||||
pub mod client;
|
||||
mod plugin_commands;
|
||||
mod providers;
|
||||
mod theme;
|
||||
@@ -65,7 +65,11 @@ fn main() {
|
||||
}
|
||||
|
||||
// No subcommand - launch the app
|
||||
let default_level = if cfg!(feature = "dev-logging") { "debug" } else { "info" };
|
||||
let default_level = if cfg!(feature = "dev-logging") {
|
||||
"debug"
|
||||
} else {
|
||||
"info"
|
||||
};
|
||||
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_level))
|
||||
.format_timestamp_millis()
|
||||
|
||||
@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
|
||||
use crate::cli::{PluginCommand as CliPluginCommand, PluginRuntime};
|
||||
use owlry_core::config::Config;
|
||||
use owlry_core::paths;
|
||||
use owlry_core::plugins::manifest::{discover_plugins, PluginManifest};
|
||||
use owlry_core::plugins::manifest::{PluginManifest, discover_plugins};
|
||||
use owlry_core::plugins::registry::{self, RegistryClient};
|
||||
use owlry_core::plugins::runtime_loader::{lua_runtime_available, rune_runtime_available};
|
||||
|
||||
@@ -46,15 +46,30 @@ fn any_runtime_available() -> bool {
|
||||
/// Execute a plugin command
|
||||
pub fn execute(cmd: CliPluginCommand) -> CommandResult {
|
||||
match cmd {
|
||||
CliPluginCommand::List { enabled, disabled, runtime, available, refresh, json } => {
|
||||
CliPluginCommand::List {
|
||||
enabled,
|
||||
disabled,
|
||||
runtime,
|
||||
available,
|
||||
refresh,
|
||||
json,
|
||||
} => {
|
||||
if available {
|
||||
cmd_list_available(refresh, json)
|
||||
} else {
|
||||
cmd_list_installed(enabled, disabled, runtime, json)
|
||||
}
|
||||
}
|
||||
CliPluginCommand::Search { query, refresh, json } => cmd_search(&query, refresh, json),
|
||||
CliPluginCommand::Info { name, registry, json } => {
|
||||
CliPluginCommand::Search {
|
||||
query,
|
||||
refresh,
|
||||
json,
|
||||
} => cmd_search(&query, refresh, json),
|
||||
CliPluginCommand::Info {
|
||||
name,
|
||||
registry,
|
||||
json,
|
||||
} => {
|
||||
if registry {
|
||||
cmd_info_registry(&name, json)
|
||||
} else {
|
||||
@@ -74,15 +89,29 @@ pub fn execute(cmd: CliPluginCommand) -> CommandResult {
|
||||
CliPluginCommand::Update { name } => cmd_update(name.as_deref()),
|
||||
CliPluginCommand::Enable { name } => cmd_enable(&name),
|
||||
CliPluginCommand::Disable { name } => cmd_disable(&name),
|
||||
CliPluginCommand::Create { name, runtime, dir, display_name, description } => {
|
||||
CliPluginCommand::Create {
|
||||
name,
|
||||
runtime,
|
||||
dir,
|
||||
display_name,
|
||||
description,
|
||||
} => {
|
||||
check_runtime_available(runtime)?;
|
||||
cmd_create(&name, runtime, dir.as_deref(), display_name.as_deref(), description.as_deref())
|
||||
cmd_create(
|
||||
&name,
|
||||
runtime,
|
||||
dir.as_deref(),
|
||||
display_name.as_deref(),
|
||||
description.as_deref(),
|
||||
)
|
||||
}
|
||||
CliPluginCommand::Validate { path } => cmd_validate(path.as_deref()),
|
||||
CliPluginCommand::Runtimes => cmd_runtimes(),
|
||||
CliPluginCommand::Run { plugin_id, command, args } => {
|
||||
cmd_run_plugin_command(&plugin_id, &command, &args)
|
||||
}
|
||||
CliPluginCommand::Run {
|
||||
plugin_id,
|
||||
command,
|
||||
args,
|
||||
} => cmd_run_plugin_command(&plugin_id, &command, &args),
|
||||
CliPluginCommand::Commands { plugin_id } => cmd_list_commands(plugin_id.as_deref()),
|
||||
}
|
||||
}
|
||||
@@ -351,7 +380,10 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
|
||||
});
|
||||
println!("{}", serde_json::to_string_pretty(&info).unwrap());
|
||||
} else {
|
||||
println!("Plugin: {} v{}", manifest.plugin.name, manifest.plugin.version);
|
||||
println!(
|
||||
"Plugin: {} v{}",
|
||||
manifest.plugin.name, manifest.plugin.version
|
||||
);
|
||||
println!("ID: {}", manifest.plugin.id);
|
||||
if !manifest.plugin.description.is_empty() {
|
||||
println!("Description: {}", manifest.plugin.description);
|
||||
@@ -359,11 +391,18 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
|
||||
if !manifest.plugin.author.is_empty() {
|
||||
println!("Author: {}", manifest.plugin.author);
|
||||
}
|
||||
println!("Status: {}", if is_enabled { "enabled" } else { "disabled" });
|
||||
println!(
|
||||
"Status: {}",
|
||||
if is_enabled { "enabled" } else { "disabled" }
|
||||
);
|
||||
println!(
|
||||
"Runtime: {}{}",
|
||||
runtime,
|
||||
if runtime_available { "" } else { " (NOT INSTALLED)" }
|
||||
if runtime_available {
|
||||
""
|
||||
} else {
|
||||
" (NOT INSTALLED)"
|
||||
}
|
||||
);
|
||||
println!("Path: {}", plugin_path.display());
|
||||
println!();
|
||||
@@ -382,12 +421,25 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
|
||||
}
|
||||
println!();
|
||||
println!("Permissions:");
|
||||
println!(" Network: {}", if manifest.permissions.network { "yes" } else { "no" });
|
||||
println!(
|
||||
" Network: {}",
|
||||
if manifest.permissions.network {
|
||||
"yes"
|
||||
} else {
|
||||
"no"
|
||||
}
|
||||
);
|
||||
if !manifest.permissions.filesystem.is_empty() {
|
||||
println!(" Filesystem: {}", manifest.permissions.filesystem.join(", "));
|
||||
println!(
|
||||
" Filesystem: {}",
|
||||
manifest.permissions.filesystem.join(", ")
|
||||
);
|
||||
}
|
||||
if !manifest.permissions.run_commands.is_empty() {
|
||||
println!(" Commands: {}", manifest.permissions.run_commands.join(", "));
|
||||
println!(
|
||||
" Commands: {}",
|
||||
manifest.permissions.run_commands.join(", ")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,7 +450,8 @@ fn cmd_info_installed(name: &str, json_output: bool) -> CommandResult {
|
||||
fn cmd_info_registry(name: &str, json_output: bool) -> CommandResult {
|
||||
let client = get_registry_client();
|
||||
|
||||
let plugin = client.find(name, false)?
|
||||
let plugin = client
|
||||
.find(name, false)?
|
||||
.ok_or_else(|| format!("Plugin '{}' not found in registry", name))?;
|
||||
|
||||
if json_output {
|
||||
@@ -466,12 +519,10 @@ fn cmd_install(source: &str, force: bool) -> CommandResult {
|
||||
println!("Found: {} v{}", plugin.name, plugin.version);
|
||||
install_from_git(&plugin.repository, &plugins_dir, force)
|
||||
}
|
||||
None => {
|
||||
Err(format!(
|
||||
"Plugin '{}' not found in registry. Use a local path or git URL.",
|
||||
source
|
||||
))
|
||||
}
|
||||
None => Err(format!(
|
||||
"Plugin '{}' not found in registry. Use a local path or git URL.",
|
||||
source
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -597,8 +648,7 @@ fn cmd_remove(name: &str, yes: bool) -> CommandResult {
|
||||
}
|
||||
}
|
||||
|
||||
fs::remove_dir_all(&plugin_path)
|
||||
.map_err(|e| format!("Failed to remove plugin: {}", e))?;
|
||||
fs::remove_dir_all(&plugin_path).map_err(|e| format!("Failed to remove plugin: {}", e))?;
|
||||
|
||||
// Also remove from disabled list if present
|
||||
if let Ok(mut config) = Config::load() {
|
||||
@@ -645,7 +695,9 @@ fn cmd_enable(name: &str) -> CommandResult {
|
||||
}
|
||||
|
||||
config.plugins.disabled_plugins.retain(|id| id != name);
|
||||
config.save().map_err(|e| format!("Failed to save config: {}", e))?;
|
||||
config
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save config: {}", e))?;
|
||||
|
||||
println!("Enabled plugin '{}'", name);
|
||||
Ok(())
|
||||
@@ -668,7 +720,9 @@ fn cmd_disable(name: &str) -> CommandResult {
|
||||
}
|
||||
|
||||
config.plugins.disabled_plugins.push(name.to_string());
|
||||
config.save().map_err(|e| format!("Failed to save config: {}", e))?;
|
||||
config
|
||||
.save()
|
||||
.map_err(|e| format!("Failed to save config: {}", e))?;
|
||||
|
||||
println!("Disabled plugin '{}'", name);
|
||||
Ok(())
|
||||
@@ -688,11 +742,13 @@ fn cmd_create(
|
||||
let plugin_dir = base_dir.join(name);
|
||||
|
||||
if plugin_dir.exists() {
|
||||
return Err(format!("Directory '{}' already exists", plugin_dir.display()));
|
||||
return Err(format!(
|
||||
"Directory '{}' already exists",
|
||||
plugin_dir.display()
|
||||
));
|
||||
}
|
||||
|
||||
fs::create_dir_all(&plugin_dir)
|
||||
.map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
fs::create_dir_all(&plugin_dir).map_err(|e| format!("Failed to create directory: {}", e))?;
|
||||
|
||||
let display = display_name.unwrap_or(name);
|
||||
let desc = description.unwrap_or("A custom owlry plugin");
|
||||
@@ -825,14 +881,28 @@ pub fn register(owlry) {{{{
|
||||
}
|
||||
}
|
||||
|
||||
println!("Created {} plugin '{}' at {}", runtime, name, plugin_dir.display());
|
||||
println!(
|
||||
"Created {} plugin '{}' at {}",
|
||||
runtime,
|
||||
name,
|
||||
plugin_dir.display()
|
||||
);
|
||||
println!();
|
||||
println!("Next steps:");
|
||||
println!(" 1. Edit {}/{} to implement your provider", name, entry_file);
|
||||
println!(" 2. Install: owlry plugin install {}", plugin_dir.display());
|
||||
println!(
|
||||
" 1. Edit {}/{} to implement your provider",
|
||||
name, entry_file
|
||||
);
|
||||
println!(
|
||||
" 2. Install: owlry plugin install {}",
|
||||
plugin_dir.display()
|
||||
);
|
||||
println!(" 3. Test: owlry (your plugin items should appear)");
|
||||
println!();
|
||||
println!("Runtime: {} (requires owlry-{} package)", runtime, entry_ext);
|
||||
println!(
|
||||
"Runtime: {} (requires owlry-{} package)",
|
||||
runtime, entry_ext
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -996,15 +1066,29 @@ fn cmd_run_plugin_command(plugin_id: &str, command: &str, args: &[String]) -> Co
|
||||
.map_err(|e| format!("Failed to parse manifest: {}", e))?;
|
||||
|
||||
// Check if plugin provides this command
|
||||
let cmd_info = manifest.provides.commands.iter().find(|c| c.name == command);
|
||||
let cmd_info = manifest
|
||||
.provides
|
||||
.commands
|
||||
.iter()
|
||||
.find(|c| c.name == command);
|
||||
if cmd_info.is_none() {
|
||||
let available: Vec<_> = manifest.provides.commands.iter().map(|c| c.name.as_str()).collect();
|
||||
let available: Vec<_> = manifest
|
||||
.provides
|
||||
.commands
|
||||
.iter()
|
||||
.map(|c| c.name.as_str())
|
||||
.collect();
|
||||
if available.is_empty() {
|
||||
return Err(format!("Plugin '{}' does not provide any CLI commands", plugin_id));
|
||||
return Err(format!(
|
||||
"Plugin '{}' does not provide any CLI commands",
|
||||
plugin_id
|
||||
));
|
||||
}
|
||||
return Err(format!(
|
||||
"Plugin '{}' does not have command '{}'. Available: {}",
|
||||
plugin_id, command, available.join(", ")
|
||||
plugin_id,
|
||||
command,
|
||||
available.join(", ")
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1030,10 +1114,8 @@ fn execute_plugin_command(
|
||||
|
||||
// Load the appropriate runtime
|
||||
let loaded_runtime = match runtime {
|
||||
PluginRuntime::Lua => {
|
||||
LoadedRuntime::load_lua(plugin_path.parent().unwrap_or(plugin_path))
|
||||
.map_err(|e| format!("Failed to load Lua runtime: {}", e))?
|
||||
}
|
||||
PluginRuntime::Lua => LoadedRuntime::load_lua(plugin_path.parent().unwrap_or(plugin_path))
|
||||
.map_err(|e| format!("Failed to load Lua runtime: {}", e))?,
|
||||
PluginRuntime::Rune => {
|
||||
LoadedRuntime::load_rune(plugin_path.parent().unwrap_or(plugin_path))
|
||||
.map_err(|e| format!("Failed to load Rune runtime: {}", e))?
|
||||
@@ -1047,7 +1129,10 @@ fn execute_plugin_command(
|
||||
let _query = query_parts.join(":");
|
||||
|
||||
// Find the provider from this plugin and send the command query
|
||||
let _provider_name = manifest.provides.providers.first()
|
||||
let _provider_name = manifest
|
||||
.provides
|
||||
.providers
|
||||
.first()
|
||||
.ok_or_else(|| format!("Plugin '{}' has no providers", manifest.plugin.id))?;
|
||||
|
||||
// Query the provider with the command
|
||||
@@ -1056,14 +1141,31 @@ fn execute_plugin_command(
|
||||
|
||||
// For now, we use a simpler approach: invoke the entry point with command args
|
||||
// This requires runtime support for command execution
|
||||
println!("Executing: owlry plugin run {} {} {}", manifest.plugin.id, command, args.join(" "));
|
||||
println!(
|
||||
"Executing: owlry plugin run {} {} {}",
|
||||
manifest.plugin.id,
|
||||
command,
|
||||
args.join(" ")
|
||||
);
|
||||
println!();
|
||||
println!("Note: Plugin command execution requires runtime support.");
|
||||
println!("The plugin entry point should handle CLI commands via owlry.command.register()");
|
||||
println!();
|
||||
println!("Runtime: {} ({})", runtime, if PathBuf::from(SYSTEM_RUNTIMES_DIR).join(
|
||||
match runtime { PluginRuntime::Lua => "liblua.so", PluginRuntime::Rune => "librune.so" }
|
||||
).exists() { "available" } else { "NOT INSTALLED" });
|
||||
println!(
|
||||
"Runtime: {} ({})",
|
||||
runtime,
|
||||
if PathBuf::from(SYSTEM_RUNTIMES_DIR)
|
||||
.join(match runtime {
|
||||
PluginRuntime::Lua => "liblua.so",
|
||||
PluginRuntime::Rune => "librune.so",
|
||||
})
|
||||
.exists()
|
||||
{
|
||||
"available"
|
||||
} else {
|
||||
"NOT INSTALLED"
|
||||
}
|
||||
);
|
||||
|
||||
// TODO: Implement actual command execution through runtime
|
||||
// This would involve:
|
||||
@@ -1087,7 +1189,8 @@ fn cmd_list_commands(plugin_id: Option<&str>) -> CommandResult {
|
||||
|
||||
if let Some(id) = plugin_id {
|
||||
// Show commands for a specific plugin
|
||||
let (manifest, _path) = discovered.get(id)
|
||||
let (manifest, _path) = discovered
|
||||
.get(id)
|
||||
.ok_or_else(|| format!("Plugin '{}' not found", id))?;
|
||||
|
||||
if manifest.provides.commands.is_empty() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use owlry_core::providers::{LaunchItem, Provider, ProviderType};
|
||||
use log::debug;
|
||||
use owlry_core::providers::{LaunchItem, Provider, ProviderType};
|
||||
use std::io::{self, BufRead};
|
||||
|
||||
/// Provider for dmenu-style input from stdin
|
||||
|
||||
@@ -6,7 +6,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
||||
|
||||
// Always inject layout config values
|
||||
css.push_str(&format!(" --owlry-font-size: {}px;\n", config.font_size));
|
||||
css.push_str(&format!(" --owlry-border-radius: {}px;\n", config.border_radius));
|
||||
css.push_str(&format!(
|
||||
" --owlry-border-radius: {}px;\n",
|
||||
config.border_radius
|
||||
));
|
||||
|
||||
// Only inject colors if user specified them
|
||||
if let Some(ref bg) = config.colors.background {
|
||||
@@ -22,7 +25,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
||||
css.push_str(&format!(" --owlry-text: {};\n", text));
|
||||
}
|
||||
if let Some(ref text_secondary) = config.colors.text_secondary {
|
||||
css.push_str(&format!(" --owlry-text-secondary: {};\n", text_secondary));
|
||||
css.push_str(&format!(
|
||||
" --owlry-text-secondary: {};\n",
|
||||
text_secondary
|
||||
));
|
||||
}
|
||||
if let Some(ref accent) = config.colors.accent {
|
||||
css.push_str(&format!(" --owlry-accent: {};\n", accent));
|
||||
@@ -36,7 +42,10 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
||||
css.push_str(&format!(" --owlry-badge-app: {};\n", badge_app));
|
||||
}
|
||||
if let Some(ref badge_bookmark) = config.colors.badge_bookmark {
|
||||
css.push_str(&format!(" --owlry-badge-bookmark: {};\n", badge_bookmark));
|
||||
css.push_str(&format!(
|
||||
" --owlry-badge-bookmark: {};\n",
|
||||
badge_bookmark
|
||||
));
|
||||
}
|
||||
if let Some(ref badge_calc) = config.colors.badge_calc {
|
||||
css.push_str(&format!(" --owlry-badge-calc: {};\n", badge_calc));
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
use crate::backend::SearchBackend;
|
||||
use owlry_core::config::Config;
|
||||
use owlry_core::filter::ProviderFilter;
|
||||
use owlry_core::providers::{LaunchItem, ProviderType};
|
||||
use crate::ui::submenu;
|
||||
use crate::ui::ResultRow;
|
||||
use crate::ui::submenu;
|
||||
use gtk4::gdk::Key;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{
|
||||
@@ -11,6 +8,9 @@ use gtk4::{
|
||||
ListBoxRow, Orientation, ScrolledWindow, SelectionMode, ToggleButton,
|
||||
};
|
||||
use log::info;
|
||||
use owlry_core::config::Config;
|
||||
use owlry_core::filter::ProviderFilter;
|
||||
use owlry_core::providers::{LaunchItem, ProviderType};
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
use log::debug;
|
||||
@@ -148,7 +148,9 @@ impl MainWindow {
|
||||
header_box.append(&filter_tabs);
|
||||
|
||||
// Search entry with dynamic placeholder (or custom prompt if provided)
|
||||
let placeholder = custom_prompt.clone().unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
|
||||
let placeholder = custom_prompt
|
||||
.clone()
|
||||
.unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
|
||||
let search_entry = Entry::builder()
|
||||
.placeholder_text(&placeholder)
|
||||
.hexpand(true)
|
||||
@@ -293,8 +295,16 @@ impl MainWindow {
|
||||
// Show number hint in the label for first 9 tabs (using superscript)
|
||||
let label = if idx < 9 {
|
||||
let superscript = match idx + 1 {
|
||||
1 => "¹", 2 => "²", 3 => "³", 4 => "⁴", 5 => "⁵",
|
||||
6 => "⁶", 7 => "⁷", 8 => "⁸", 9 => "⁹", _ => "",
|
||||
1 => "¹",
|
||||
2 => "²",
|
||||
3 => "³",
|
||||
4 => "⁴",
|
||||
5 => "⁵",
|
||||
6 => "⁶",
|
||||
7 => "⁷",
|
||||
8 => "⁸",
|
||||
9 => "⁹",
|
||||
_ => "",
|
||||
};
|
||||
format!("{}{}", base_label, superscript)
|
||||
} else {
|
||||
@@ -494,7 +504,11 @@ impl MainWindow {
|
||||
actions: Vec<LaunchItem>,
|
||||
) {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Entering submenu: {} ({} actions)", display_name, actions.len());
|
||||
debug!(
|
||||
"[UI] Entering submenu: {} ({} actions)",
|
||||
display_name,
|
||||
actions.len()
|
||||
);
|
||||
|
||||
// Save current state
|
||||
{
|
||||
@@ -705,7 +719,8 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
// current_results holds only what's displayed (for selection/activation)
|
||||
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
|
||||
*current_results.borrow_mut() =
|
||||
results.into_iter().take(initial_count).collect();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -736,15 +751,19 @@ impl MainWindow {
|
||||
if let Some(item) = results.get(index) {
|
||||
// Check if this is a submenu item and query the plugin for actions
|
||||
let submenu_result = if submenu::is_submenu_item(item) {
|
||||
if let Some((plugin_id, data)) = submenu::parse_submenu_command(&item.command) {
|
||||
if let Some((plugin_id, data)) =
|
||||
submenu::parse_submenu_command(&item.command)
|
||||
{
|
||||
// Clone values before dropping borrow
|
||||
let plugin_id = plugin_id.to_string();
|
||||
let data = data.to_string();
|
||||
let display_name = item.name.clone();
|
||||
drop(results); // Release borrow before querying
|
||||
backend_for_activate
|
||||
.borrow_mut()
|
||||
.query_submenu_actions(&plugin_id, &data, &display_name)
|
||||
backend_for_activate.borrow_mut().query_submenu_actions(
|
||||
&plugin_id,
|
||||
&data,
|
||||
&display_name,
|
||||
)
|
||||
} else {
|
||||
drop(results);
|
||||
None
|
||||
@@ -843,7 +862,10 @@ impl MainWindow {
|
||||
let shift = modifiers.contains(gtk4::gdk::ModifierType::SHIFT_MASK);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Key pressed: {:?} (ctrl={}, shift={})", key, ctrl, shift);
|
||||
debug!(
|
||||
"[UI] Key pressed: {:?} (ctrl={}, shift={})",
|
||||
key, ctrl, shift
|
||||
);
|
||||
|
||||
match key {
|
||||
Key::Escape => {
|
||||
@@ -906,10 +928,11 @@ impl MainWindow {
|
||||
if let Some(selected) = results_list.selected_row() {
|
||||
let prev_index = selected.index() - 1;
|
||||
if prev_index >= 0
|
||||
&& let Some(prev_row) = results_list.row_at_index(prev_index) {
|
||||
results_list.select_row(Some(&prev_row));
|
||||
Self::scroll_to_row(&scrolled, &results_list, &prev_row);
|
||||
}
|
||||
&& let Some(prev_row) = results_list.row_at_index(prev_index)
|
||||
{
|
||||
results_list.select_row(Some(&prev_row));
|
||||
Self::scroll_to_row(&scrolled, &results_list, &prev_row);
|
||||
}
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
@@ -941,8 +964,17 @@ impl MainWindow {
|
||||
gtk4::glib::Propagation::Stop
|
||||
}
|
||||
// Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu)
|
||||
Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 |
|
||||
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
|
||||
Key::_1
|
||||
| Key::_2
|
||||
| Key::_3
|
||||
| Key::_4
|
||||
| Key::_5
|
||||
| Key::_6
|
||||
| Key::_7
|
||||
| Key::_8
|
||||
| Key::_9
|
||||
if ctrl =>
|
||||
{
|
||||
info!("[UI] Ctrl+number detected: {:?}", key);
|
||||
if !submenu_state.borrow().active {
|
||||
let idx = match key {
|
||||
@@ -968,7 +1000,11 @@ impl MainWindow {
|
||||
&mode_label,
|
||||
);
|
||||
} else {
|
||||
info!("[UI] No provider at index {}, tab_order len={}", idx, tab_order.len());
|
||||
info!(
|
||||
"[UI] No provider at index {}, tab_order len={}",
|
||||
idx,
|
||||
tab_order.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
@@ -1029,7 +1065,8 @@ impl MainWindow {
|
||||
let results = current_results.borrow();
|
||||
if let Some(item) = results.get(index).cloned() {
|
||||
drop(results);
|
||||
let should_close = Self::handle_item_action(&item, &config.borrow(), &backend);
|
||||
let should_close =
|
||||
Self::handle_item_action(&item, &config.borrow(), &backend);
|
||||
if should_close {
|
||||
window.close();
|
||||
} else {
|
||||
@@ -1076,7 +1113,11 @@ impl MainWindow {
|
||||
}
|
||||
} else if current.len() == 1 {
|
||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
let at_boundary = if forward { idx == tab_order.len() - 1 } else { idx == 0 };
|
||||
let at_boundary = if forward {
|
||||
idx == tab_order.len() - 1
|
||||
} else {
|
||||
idx == 0
|
||||
};
|
||||
|
||||
if at_boundary {
|
||||
// At boundary, go back to "All" mode
|
||||
@@ -1284,11 +1325,14 @@ impl MainWindow {
|
||||
info!("Launching: {} ({})", item.name, item.command);
|
||||
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[UI] Launch details: terminal={}, provider={:?}, id={}", item.terminal, item.provider, item.id);
|
||||
debug!(
|
||||
"[UI] Launch details: terminal={}, provider={:?}, id={}",
|
||||
item.terminal, item.provider, item.id
|
||||
);
|
||||
|
||||
// Check if this is a desktop application (has .desktop file as ID)
|
||||
let is_desktop_app = matches!(item.provider, ProviderType::Application)
|
||||
&& item.id.ends_with(".desktop");
|
||||
let is_desktop_app =
|
||||
matches!(item.provider, ProviderType::Application) && item.id.ends_with(".desktop");
|
||||
|
||||
// Desktop files should be launched via proper launchers that implement the
|
||||
// freedesktop Desktop Entry spec (D-Bus activation, field codes, env vars, etc.)
|
||||
@@ -1315,7 +1359,10 @@ impl MainWindow {
|
||||
///
|
||||
/// Otherwise, uses `gio launch` which is always available (part of glib2/GTK4)
|
||||
/// and handles D-Bus activation, field codes, Terminal flag, etc.
|
||||
fn launch_desktop_file(desktop_path: &str, config: &Config) -> std::io::Result<std::process::Child> {
|
||||
fn launch_desktop_file(
|
||||
desktop_path: &str,
|
||||
config: &Config,
|
||||
) -> std::io::Result<std::process::Child> {
|
||||
use std::path::Path;
|
||||
|
||||
// Check if desktop file exists
|
||||
@@ -1349,16 +1396,22 @@ impl MainWindow {
|
||||
.spawn()
|
||||
} else {
|
||||
info!("Launching via gio: {}", desktop_path);
|
||||
Command::new("gio")
|
||||
.args(["launch", desktop_path])
|
||||
.spawn()
|
||||
Command::new("gio").args(["launch", desktop_path]).spawn()
|
||||
}
|
||||
}
|
||||
|
||||
/// Launch a shell command (for non-desktop items like PATH commands, plugins, etc.)
|
||||
fn launch_command(command: &str, terminal: bool, config: &Config) -> std::io::Result<std::process::Child> {
|
||||
fn launch_command(
|
||||
command: &str,
|
||||
terminal: bool,
|
||||
config: &Config,
|
||||
) -> std::io::Result<std::process::Child> {
|
||||
let cmd = if terminal {
|
||||
let terminal_cmd = config.general.terminal_command.as_deref().unwrap_or("xterm");
|
||||
let terminal_cmd = config
|
||||
.general
|
||||
.terminal_command
|
||||
.as_deref()
|
||||
.unwrap_or("xterm");
|
||||
format!("{} -e {}", terminal_cmd, command)
|
||||
} else {
|
||||
command.to_string()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use owlry_core::providers::LaunchItem;
|
||||
use gtk4::prelude::*;
|
||||
use gtk4::{Box as GtkBox, Image, Label, ListBoxRow, Orientation, Widget};
|
||||
use owlry_core::providers::LaunchItem;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub struct ResultRow {
|
||||
@@ -81,7 +81,9 @@ impl ResultRow {
|
||||
} else {
|
||||
// Default icon based on provider type (only core types, plugins should provide icons)
|
||||
let default_icon = match &item.provider {
|
||||
owlry_core::providers::ProviderType::Application => "application-x-executable-symbolic",
|
||||
owlry_core::providers::ProviderType::Application => {
|
||||
"application-x-executable-symbolic"
|
||||
}
|
||||
owlry_core::providers::ProviderType::Command => "utilities-terminal-symbolic",
|
||||
owlry_core::providers::ProviderType::Dmenu => "view-list-symbolic",
|
||||
// Plugins should provide their own icon; fallback to generic addon icon
|
||||
@@ -134,9 +136,7 @@ impl ResultRow {
|
||||
.build();
|
||||
|
||||
for tag in item.tags.iter().take(3) {
|
||||
let tag_label = Label::builder()
|
||||
.label(tag)
|
||||
.build();
|
||||
let tag_label = Label::builder().label(tag).build();
|
||||
tag_label.add_css_class("owlry-tag-badge");
|
||||
tags_box.append(&tag_label);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user