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