From d7385a23ad8ad020b49551798c8b44ee683bc109 Mon Sep 17 00:00:00 2001 From: Devyn Cairns Date: Mon, 1 Apr 2024 02:02:33 -0700 Subject: [PATCH] implement latest nushell api updates --- src/client.rs | 91 +++++----- src/commands/call.rs | 127 ++++++++++++++ src/commands/get.rs | 85 +++++++++ src/commands/get_all.rs | 76 ++++++++ src/commands/introspect.rs | 84 +++++++++ src/commands/list.rs | 90 ++++++++++ src/commands/main.rs | 36 ++++ src/commands/mod.rs | 15 ++ src/commands/set.rs | 95 ++++++++++ src/config.rs | 14 +- src/convert.rs | 61 +++---- src/main.rs | 349 ++----------------------------------- 12 files changed, 698 insertions(+), 425 deletions(-) create mode 100644 src/commands/call.rs create mode 100644 src/commands/get.rs create mode 100644 src/commands/get_all.rs create mode 100644 src/commands/introspect.rs create mode 100644 src/commands/list.rs create mode 100644 src/commands/main.rs create mode 100644 src/commands/mod.rs create mode 100644 src/commands/set.rs diff --git a/src/client.rs b/src/client.rs index 210a677..759d225 100644 --- a/src/client.rs +++ b/src/client.rs @@ -3,8 +3,7 @@ use dbus::{ channel::{BusType, Channel}, Message, }; -use nu_plugin::LabeledError; -use nu_protocol::{Spanned, Value}; +use nu_protocol::{LabeledError, Spanned, Value}; use crate::{ config::{DbusBusChoice, DbusClientConfig}, @@ -23,11 +22,8 @@ pub struct DbusClient { // Convenience macros for error handling macro_rules! validate_with { ($type:ty, $spanned:expr) => { - <$type>::new(&$spanned.item).map_err(|msg| LabeledError { - label: msg, - msg: "this argument is incorrect".into(), - span: Some($spanned.span), - }) + <$type>::new(&$spanned.item) + .map_err(|msg| LabeledError::new("Invalid argument").with_label(msg, $spanned.span)) }; } @@ -44,10 +40,11 @@ impl DbusClient { Ok(ch) }), } - .map_err(|err| LabeledError { - label: err.to_string(), - msg: "while connecting to D-Bus as specified here".into(), - span: Some(config.bus_choice.span), + .map_err(|err| { + LabeledError::new(err.to_string()).with_label( + "while connecting to D-Bus as specified here", + config.bus_choice.span, + ) })?; Ok(DbusClient { config, @@ -56,11 +53,7 @@ impl DbusClient { } fn error(&self, err: impl std::fmt::Display, msg: impl std::fmt::Display) -> LabeledError { - LabeledError { - label: err.to_string(), - msg: msg.to_string(), - span: Some(self.config.span), - } + LabeledError::new(err.to_string()).with_label(msg.to_string(), self.config.span) } /// Introspect a D-Bus object @@ -107,20 +100,22 @@ impl DbusClient { let node = self.introspect(dest, object)?; if let Some(sig) = node.get_method_args_signature(&interface.item, &method.item) { - DbusType::parse_all(&sig).map_err(|err| LabeledError { - label: format!( + DbusType::parse_all(&sig).map_err(|err| { + LabeledError::new(format!( "while getting interface {:?} method {:?} signature: {}", interface.item, method.item, err - ), - msg: "try running with --no-introspect or --signature".into(), - span: Some(self.config.span), + )) + .with_label( + "try running with --no-introspect or --signature", + self.config.span, + ) }) } else { - Err(LabeledError { - label: format!("Method {:?} not found on {:?}", method.item, interface.item), - msg: "check that this method/interface is correct".into(), - span: Some(method.span), - }) + Err(LabeledError::new(format!( + "Method {:?} not found on {:?}", + method.item, interface.item + )) + .with_label("check that this method/interface is correct", method.span)) } } @@ -135,23 +130,25 @@ impl DbusClient { let node = self.introspect(dest, object)?; if let Some(sig) = node.get_property_signature(&interface.item, &property.item) { - DbusType::parse_all(sig).map_err(|err| LabeledError { - label: format!( + DbusType::parse_all(sig).map_err(|err| { + LabeledError::new(format!( "while getting interface {:?} property {:?} signature: {}", interface.item, property.item, err - ), - msg: "try running with --no-introspect or --signature".into(), - span: Some(self.config.span), + )) + .with_label( + "try running with --no-introspect or --signature", + self.config.span, + ) }) } else { - Err(LabeledError { - label: format!( - "Property {:?} not found on {:?}", - property.item, interface.item - ), - msg: "check that this property/interface is correct".into(), - span: Some(property.span), - }) + Err(LabeledError::new(format!( + "Property {:?} not found on {:?}", + property.item, interface.item + )) + .with_label( + "check that this property or interface is correct", + property.span, + )) } } @@ -176,10 +173,8 @@ impl DbusClient { // Parse the signature let mut valid_signature = signature .map(|s| { - DbusType::parse_all(&s.item).map_err(|err| LabeledError { - label: err, - msg: "in signature specified here".into(), - span: Some(s.span), + DbusType::parse_all(&s.item).map_err(|err| { + LabeledError::new(err).with_label("in signature specified here", s.span) }) }) .transpose()?; @@ -195,7 +190,7 @@ impl DbusClient { "Warning: D-Bus introspection failed on {:?}. \ Use `--no-introspect` or pass `--signature` to silence this warning. \ Cause: {}", - object.item, err.label + object.item, err ); } } @@ -314,10 +309,8 @@ impl DbusClient { // Parse the signature let mut valid_signature = signature .map(|s| { - DbusType::parse_all(&s.item).map_err(|err| LabeledError { - label: err, - msg: "in signature specified here".into(), - span: Some(s.span), + DbusType::parse_all(&s.item).map_err(|err| { + LabeledError::new(err).with_label("in signature specified here", s.span) }) }) .transpose()?; @@ -333,7 +326,7 @@ impl DbusClient { "Warning: D-Bus introspection failed on {:?}. \ Use `--no-introspect` or pass `--signature` to silence this warning. \ Cause: {}", - object.item, err.label + object.item, err ); } } diff --git a/src/commands/call.rs b/src/commands/call.rs new file mode 100644 index 0000000..61bb06b --- /dev/null +++ b/src/commands/call.rs @@ -0,0 +1,127 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Example, LabeledError, Signature, SyntaxShape, Type, Value}; + +use crate::{client::DbusClient, config::DbusClientConfig, DbusSignatureUtilExt}; + +pub struct Call; + +impl SimplePluginCommand for Call { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus call" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .dbus_command() + .accepts_dbus_client_options() + .accepts_timeout() + .input_output_type(Type::Nothing, Type::Any) + .named( + "signature", + SyntaxShape::String, + "Signature of the arguments to send, in D-Bus format.\n \ + If not provided, they will be determined from introspection.\n \ + If --no-introspect is specified and this is not provided, they will \ + be guessed (poorly)", + None, + ) + .switch( + "no-flatten", + "Always return a list of all return values", + None, + ) + .switch( + "no-introspect", + "Don't use introspection to determine the correct argument signature", + None, + ) + .required_named( + "dest", + SyntaxShape::String, + "The name of the connection to send the method to", + None, + ) + .required( + "object", + SyntaxShape::String, + "The path to the object to call the method on", + ) + .required( + "interface", + SyntaxShape::String, + "The name of the interface the method belongs to", + ) + .required( + "method", + SyntaxShape::String, + "The name of the method to send", + ) + .rest( + "args", + SyntaxShape::Any, + "Arguments to send with the method call", + ) + } + + fn usage(&self) -> &str { + "Call a method and get its response" + } + + fn extra_usage(&self) -> &str { + "Returns an array if the method call returns more than one value." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "dbus call --dest=org.freedesktop.DBus \ + /org/freedesktop/DBus org.freedesktop.DBus.Peer Ping", + description: "Ping the D-Bus server itself", + result: None, + }, + Example { + example: "dbus call --dest=org.freedesktop.Notifications \ + /org/freedesktop/Notifications org.freedesktop.Notifications \ + Notify \"Floppy disks\" 0 \"media-floppy\" \"Rarely seen\" \ + \"But sometimes still used\" [] {} 5000", + description: "Show a notification on the desktop for 5 seconds", + result: None, + }, + ] + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = DbusClientConfig::try_from(call)?; + let dbus = DbusClient::new(config)?; + let values = dbus.call( + &call.get_flag("dest")?.unwrap(), + &call.req(0)?, + &call.req(1)?, + &call.req(2)?, + call.get_flag("signature")?.as_ref(), + &call.positional[3..], + )?; + + let flatten = !call.get_flag::("no-flatten")?.unwrap_or(false); + + // Make the output easier to deal with by returning a list only if there are multiple return + // values (not so common) + match values.len() { + 0 if flatten => Ok(Value::nothing(call.head)), + 1 if flatten => Ok(values.into_iter().nth(0).unwrap()), + _ => Ok(Value::list(values, call.head)), + } + } +} diff --git a/src/commands/get.rs b/src/commands/get.rs new file mode 100644 index 0000000..9d68fa1 --- /dev/null +++ b/src/commands/get.rs @@ -0,0 +1,85 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Example, LabeledError, Signature, SyntaxShape, Type, Value}; + +use crate::{client::DbusClient, config::DbusClientConfig, DbusSignatureUtilExt}; + +pub struct Get; + +impl SimplePluginCommand for Get { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus get" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .dbus_command() + .accepts_dbus_client_options() + .accepts_timeout() + .input_output_type(Type::Nothing, Type::Any) + .required_named( + "dest", + SyntaxShape::String, + "The name of the connection to read the property from", + None, + ) + .required( + "object", + SyntaxShape::String, + "The path to the object to read the property from", + ) + .required( + "interface", + SyntaxShape::String, + "The name of the interface the property belongs to", + ) + .required( + "property", + SyntaxShape::String, + "The name of the property to read", + ) + } + + fn usage(&self) -> &str { + "Get a D-Bus property" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus", "property", "read"] + } + + fn examples(&self) -> Vec { + vec![Example { + example: "dbus get --dest=org.mpris.MediaPlayer2.spotify \ + /org/mpris/MediaPlayer2 \ + org.mpris.MediaPlayer2.Player Metadata", + description: "Get the currently playing song in Spotify", + result: Some(Value::test_record(nu_protocol::record!( + "xesam:title" => Value::test_string("Birdie"), + "xesam:artist" => Value::test_list(vec![ + Value::test_string("LOVE PSYCHEDELICO") + ]), + "xesam:album" => Value::test_string("Love Your Love"), + "xesam:url" => Value::test_string("https://open.spotify.com/track/51748BvzeeMs4PIdPuyZmv"), + ))), + }] + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = DbusClientConfig::try_from(call)?; + let dbus = DbusClient::new(config)?; + dbus.get( + &call.get_flag("dest")?.unwrap(), + &call.req(0)?, + &call.req(1)?, + &call.req(2)?, + ) + } +} diff --git a/src/commands/get_all.rs b/src/commands/get_all.rs new file mode 100644 index 0000000..f149588 --- /dev/null +++ b/src/commands/get_all.rs @@ -0,0 +1,76 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Example, LabeledError, Signature, SyntaxShape, Type, Value}; + +use crate::{client::DbusClient, config::DbusClientConfig, DbusSignatureUtilExt}; + +pub struct GetAll; + +impl SimplePluginCommand for GetAll { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus get-all" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .dbus_command() + .accepts_dbus_client_options() + .accepts_timeout() + .input_output_type(Type::Nothing, Type::Record(vec![])) + .required_named( + "dest", + SyntaxShape::String, + "The name of the connection to read the property from", + None, + ) + .required( + "object", + SyntaxShape::String, + "The path to the object to read the property from", + ) + .required( + "interface", + SyntaxShape::String, + "The name of the interface the property belongs to", + ) + } + + fn usage(&self) -> &str { + "Get all D-Bus properties for the given object" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus", "properties", "property", "get"] + } + + fn examples(&self) -> Vec { + vec![Example { + example: "dbus get-all --dest=org.mpris.MediaPlayer2.spotify \ + /org/mpris/MediaPlayer2 \ + org.mpris.MediaPlayer2.Player", + description: "Get the current player state of Spotify", + result: Some(Value::test_record(nu_protocol::record!( + "CanPlay" => Value::test_bool(true), + "Volume" => Value::test_float(0.43), + "PlaybackStatus" => Value::test_string("Paused"), + ))), + }] + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = DbusClientConfig::try_from(call)?; + let dbus = DbusClient::new(config)?; + dbus.get_all( + &call.get_flag("dest")?.unwrap(), + &call.req(0)?, + &call.req(1)?, + ) + } +} diff --git a/src/commands/introspect.rs b/src/commands/introspect.rs new file mode 100644 index 0000000..036b413 --- /dev/null +++ b/src/commands/introspect.rs @@ -0,0 +1,84 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Example, LabeledError, Signature, SyntaxShape, Type, Value}; + +use crate::{client::DbusClient, config::DbusClientConfig, DbusSignatureUtilExt}; + +pub struct Introspect; + +impl SimplePluginCommand for Introspect { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus introspect" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .dbus_command() + .accepts_dbus_client_options() + .accepts_timeout() + .input_output_type(Type::Nothing, Type::Record(vec![])) + .required_named( + "dest", + SyntaxShape::String, + "The name of the connection that owns the object", + None, + ) + .required( + "object", + SyntaxShape::String, + "The path to the object to introspect", + ) + } + + fn usage(&self) -> &str { + "Introspect a D-Bus object" + } + + fn extra_usage(&self) -> &str { + "Returns information about available nodes, interfaces, methods, \ + signals, and properties on the given object path" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus", "help", "method"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "dbus introspect --dest=org.mpris.MediaPlayer2.spotify \ + /org/mpris/MediaPlayer2 | explore", + description: "Look at the MPRIS2 interfaces exposed by Spotify", + result: None, + }, + Example { + example: "dbus introspect --dest=org.kde.plasmashell \ + /org/kde/osdService | get interfaces | \ + where name == org.kde.osdService | get 0.methods", + description: "Get methods exposed by KDE Plasma's on-screen display \ + service", + result: None, + }, + Example { + example: "dbus introspect --dest=org.kde.KWin / | get children | \ + select name", + description: "List objects exposed by KWin", + result: None, + }, + ] + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = DbusClientConfig::try_from(call)?; + let dbus = DbusClient::new(config)?; + let node = dbus.introspect(&call.get_flag("dest")?.unwrap(), &call.req(0)?)?; + Ok(node.to_value(call.head)) + } +} diff --git a/src/commands/list.rs b/src/commands/list.rs new file mode 100644 index 0000000..aabacc5 --- /dev/null +++ b/src/commands/list.rs @@ -0,0 +1,90 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Example, LabeledError, Signature, SyntaxShape, Type, Value}; + +use crate::{client::DbusClient, config::DbusClientConfig, pattern::Pattern, DbusSignatureUtilExt}; + +pub struct List; + +impl SimplePluginCommand for List { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus list" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .dbus_command() + .accepts_dbus_client_options() + .accepts_timeout() + .input_output_type(Type::Nothing, Type::List(Type::String.into())) + .optional( + "pattern", + SyntaxShape::String, + "An optional glob-like pattern to filter the result by", + ) + } + + fn usage(&self) -> &str { + "List all available connection names on the bus" + } + + fn extra_usage(&self) -> &str { + "These can be used as arguments for --dest on any of the other commands." + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus", "list", "find", "search", "help"] + } + + fn examples(&self) -> Vec { + vec![ + Example { + example: "dbus list", + description: "List all names available on the bus", + result: None, + }, + Example { + example: "dbus list org.freedesktop.*", + description: "List top-level freedesktop.org names on the bus \ + (e.g. matches `org.freedesktop.PowerManagement`, \ + but not `org.freedesktop.Management.Inhibit`)", + result: Some(Value::test_list(vec![ + Value::test_string("org.freedesktop.DBus"), + Value::test_string("org.freedesktop.Flatpak"), + Value::test_string("org.freedesktop.Notifications"), + ])), + }, + Example { + example: "dbus list org.mpris.MediaPlayer2.**", + description: "List all MPRIS2 media players on the bus", + result: Some(Value::test_list(vec![ + Value::test_string("org.mpris.MediaPlayer2.spotify"), + Value::test_string("org.mpris.MediaPlayer2.kdeconnect.mpris_000001"), + ])), + }, + ] + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = DbusClientConfig::try_from(call)?; + let dbus = DbusClient::new(config)?; + let pattern = call + .opt::(0)? + .map(|pat| Pattern::new(&pat, Some('.'))); + let result = dbus.list(pattern.as_ref())?; + Ok(Value::list( + result + .into_iter() + .map(|s| Value::string(s, call.head)) + .collect(), + call.head, + )) + } +} diff --git a/src/commands/main.rs b/src/commands/main.rs new file mode 100644 index 0000000..909b145 --- /dev/null +++ b/src/commands/main.rs @@ -0,0 +1,36 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{LabeledError, Signature, Value}; + +use crate::DbusSignatureUtilExt; + +pub struct Main; + +impl SimplePluginCommand for Main { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()).dbus_command() + } + + fn usage(&self) -> &str { + "Commands for interacting with D-Bus" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus"] + } + + fn run( + &self, + _plugin: &Self::Plugin, + engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + Ok(Value::string(engine.get_help()?, call.head)) + } +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs new file mode 100644 index 0000000..ef1a65b --- /dev/null +++ b/src/commands/mod.rs @@ -0,0 +1,15 @@ +mod call; +mod get; +mod get_all; +mod introspect; +mod list; +mod main; +mod set; + +pub use call::Call; +pub use get::Get; +pub use get_all::GetAll; +pub use introspect::Introspect; +pub use list::List; +pub use main::Main; +pub use set::Set; diff --git a/src/commands/set.rs b/src/commands/set.rs new file mode 100644 index 0000000..886e2d7 --- /dev/null +++ b/src/commands/set.rs @@ -0,0 +1,95 @@ +use nu_plugin::{EngineInterface, EvaluatedCall, SimplePluginCommand}; +use nu_protocol::{Example, LabeledError, Signature, SyntaxShape, Type, Value}; + +use crate::{client::DbusClient, config::DbusClientConfig, DbusSignatureUtilExt}; + +pub struct Set; + +impl SimplePluginCommand for Set { + type Plugin = crate::NuPluginDbus; + + fn name(&self) -> &str { + "dbus set" + } + + fn signature(&self) -> Signature { + Signature::build(self.name()) + .dbus_command() + .accepts_dbus_client_options() + .accepts_timeout() + .input_output_type(Type::Nothing, Type::Nothing) + .named( + "signature", + SyntaxShape::String, + "Signature of the value to set, in D-Bus format.\n \ + If not provided, it will be determined from introspection.\n \ + If --no-introspect is specified and this is not provided, it will \ + be guessed (poorly)", + None, + ) + .required_named( + "dest", + SyntaxShape::String, + "The name of the connection to write the property on", + None, + ) + .required( + "object", + SyntaxShape::String, + "The path to the object to write the property on", + ) + .required( + "interface", + SyntaxShape::String, + "The name of the interface the property belongs to", + ) + .required( + "property", + SyntaxShape::String, + "The name of the property to write", + ) + .required( + "value", + SyntaxShape::Any, + "The value to write to the property", + ) + } + + fn usage(&self) -> &str { + "Set a D-Bus property" + } + + fn search_terms(&self) -> Vec<&str> { + vec!["dbus", "property", "write", "put"] + } + + fn examples(&self) -> Vec { + vec![Example { + example: "dbus set --dest=org.mpris.MediaPlayer2.spotify \ + /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player \ + Volume 0.5", + description: "Set the volume of Spotify to 50%", + result: None, + }] + } + + fn run( + &self, + _plugin: &Self::Plugin, + _engine: &EngineInterface, + call: &EvaluatedCall, + _input: &Value, + ) -> Result { + let config = DbusClientConfig::try_from(call)?; + let dbus = DbusClient::new(config)?; + dbus.set( + &call.get_flag("dest")?.unwrap(), + &call.req(0)?, + &call.req(1)?, + &call.req(2)?, + call.get_flag("signature")?.as_ref(), + &call.req(3)?, + )?; + Ok(Value::nothing(call.head)) + } +} diff --git a/src/config.rs b/src/config.rs index 2471ada..1980580 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,7 +1,7 @@ use std::time::Duration; -use nu_plugin::{EvaluatedCall, LabeledError}; -use nu_protocol::{Span, Spanned}; +use nu_plugin::EvaluatedCall; +use nu_protocol::{LabeledError, Span, Spanned}; /// General configuration related to the D-Bus client connection #[derive(Debug, Clone)] @@ -81,12 +81,10 @@ impl TryFrom<&EvaluatedCall> for DbusClientConfig { } "timeout" => { if let Some(value) = value { - let nanos: u64 = - value.as_duration()?.try_into().map_err(|_| LabeledError { - label: "Timeout must be a positive duration".into(), - msg: "invalid timeout specified here".into(), - span: Some(value.span()), - })?; + let nanos: u64 = value.as_duration()?.try_into().map_err(|_| { + LabeledError::new("Timeout must be a positive duration") + .with_label("invalid timeout specified here", value.span()) + })?; let item = Duration::from_nanos(nanos); config.timeout = Spanned { item, diff --git a/src/convert.rs b/src/convert.rs index 6002171..11e5e7c 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -5,8 +5,7 @@ use dbus::{ }, Message, Signature, }; -use nu_plugin::LabeledError; -use nu_protocol::{Record, Span, Value}; +use nu_protocol::{LabeledError, Record, Span, Value}; use std::str::FromStr; use crate::dbus_type::DbusType; @@ -101,13 +100,12 @@ pub fn to_message_item( // Report errors from conversion. Error must support Display macro_rules! try_convert { ($result_expr:expr) => { - $result_expr.map_err(|err| LabeledError { - label: format!( + $result_expr.map_err(|err| { + LabeledError::new(format!( "Failed to convert value to the D-Bus `{:?}` type", expected_type.unwrap() - ), - msg: err.to_string(), - span: Some(value.span()), + )) + .with_label(err.to_string(), value.span()) })? }; } @@ -209,15 +207,15 @@ pub fn to_message_item( // Struct (Value::List { vals, .. }, Some(DbusType::Struct(types))) => { if vals.len() != types.len() { - return Err(LabeledError { - label: format!( - "expected struct with {} element(s) ({:?})", - types.len(), - types - ), - msg: format!("this list has {} element(s) instead", vals.len()), - span: Some(value.span()), - }); + return Err(LabeledError::new(format!( + "expected struct with {} element(s) ({:?})", + types.len(), + types + )) + .with_label( + format!("this list has {} element(s) instead", vals.len()), + value.span(), + )); } let items = vals .iter() @@ -257,15 +255,15 @@ pub fn to_message_item( ))), // Value not compatible with expected type - (other_value, Some(expectation)) => Err(LabeledError { - label: format!( - "`{}` can not be converted to the D-Bus `{:?}` type", - other_value.get_type(), - expectation - ), - msg: format!("expected a `{:?}` here", expectation), - span: Some(other_value.span()), - }), + (other_value, Some(expectation)) => Err(LabeledError::new(format!( + "`{}` can not be converted to the D-Bus `{:?}` type", + other_value.get_type(), + expectation + )) + .with_label( + format!("expected a `{:?}` here", expectation), + other_value.span(), + )), // Automatic types (with no type expectation) (Value::String { .. }, None) => to_message_item(value, Some(&DbusType::String)), @@ -283,13 +281,10 @@ pub fn to_message_item( ), // No expected type, but can't handle this type - _ => Err(LabeledError { - label: format!( - "can not use values of type `{}` in D-Bus calls", - value.get_type() - ), - msg: "use a supported type here instead".into(), - span: Some(value.span()), - }), + _ => Err(LabeledError::new(format!( + "can not use values of type `{}` in D-Bus calls", + value.get_type() + )) + .with_label("use a supported type here instead", value.span())), } } diff --git a/src/main.rs b/src/main.rs index f6aa078..7a5a2c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,270 +1,33 @@ -use nu_plugin::{ - serve_plugin, EngineInterface, EvaluatedCall, LabeledError, MsgPackSerializer, Plugin, -}; -use nu_protocol::{PluginExample, PluginSignature, Span, SyntaxShape, Type, Value}; +use nu_plugin::{serve_plugin, MsgPackSerializer, Plugin, PluginCommand}; +use nu_protocol::SyntaxShape; mod client; +mod commands; mod config; mod convert; mod dbus_type; mod introspection; mod pattern; -use client::*; -use config::*; - -use crate::pattern::Pattern; - fn main() { serve_plugin(&NuPluginDbus, MsgPackSerializer) } /// The main plugin interface for nushell -struct NuPluginDbus; +pub struct NuPluginDbus; impl Plugin for NuPluginDbus { - fn signature(&self) -> Vec { - macro_rules! str { - ($s:expr) => { - Value::string($s, Span::unknown()) - }; - } + fn commands(&self) -> Vec>> { vec![ - PluginSignature::build("dbus") - .dbus_command() - .usage("Commands for interacting with D-Bus"), - PluginSignature::build("dbus introspect") - .dbus_command() - .accepts_dbus_client_options() - .accepts_timeout() - .usage("Introspect a D-Bus object") - .input_output_type(Type::Nothing, Type::Record(vec![])) - .extra_usage("Returns information about available nodes, interfaces, methods, \ - signals, and properties on the given object path") - .required_named("dest", SyntaxShape::String, - "The name of the connection that owns the object", - None) - .required("object", SyntaxShape::String, - "The path to the object to introspect") - .plugin_examples(vec![ - PluginExample { - example: "dbus introspect --dest=org.mpris.MediaPlayer2.spotify \ - /org/mpris/MediaPlayer2 | explore".into(), - description: "Look at the MPRIS2 interfaces exposed by Spotify".into(), - result: None, - }, - PluginExample { - example: "dbus introspect --dest=org.kde.plasmashell \ - /org/kde/osdService | get interfaces | \ - where name == org.kde.osdService | get 0.methods".into(), - description: "Get methods exposed by KDE Plasma's on-screen display \ - service".into(), - result: None, - }, - PluginExample { - example: "dbus introspect --dest=org.kde.KWin / | get children | \ - select name".into(), - description: "List objects exposed by KWin".into(), - result: None, - }, - ]), - PluginSignature::build("dbus call") - .dbus_command() - .accepts_dbus_client_options() - .accepts_timeout() - .usage("Call a method and get its response") - .extra_usage("Returns an array if the method call returns more than one value.") - .input_output_type(Type::Nothing, Type::Any) - .named("signature", SyntaxShape::String, - "Signature of the arguments to send, in D-Bus format.\n \ - If not provided, they will be determined from introspection.\n \ - If --no-introspect is specified and this is not provided, they will \ - be guessed (poorly)", None) - .switch("no-flatten", - "Always return a list of all return values", None) - .switch("no-introspect", - "Don't use introspection to determine the correct argument signature", None) - .required_named("dest", SyntaxShape::String, - "The name of the connection to send the method to", - None) - .required("object", SyntaxShape::String, - "The path to the object to call the method on") - .required("interface", SyntaxShape::String, - "The name of the interface the method belongs to") - .required("method", SyntaxShape::String, - "The name of the method to send") - .rest("args", SyntaxShape::Any, - "Arguments to send with the method call") - .plugin_examples(vec![ - PluginExample { - example: "dbus call --dest=org.freedesktop.DBus \ - /org/freedesktop/DBus org.freedesktop.DBus.Peer Ping".into(), - description: "Ping the D-Bus server itself".into(), - result: None - }, - PluginExample { - example: "dbus call --dest=org.freedesktop.Notifications \ - /org/freedesktop/Notifications org.freedesktop.Notifications \ - Notify \"Floppy disks\" 0 \"media-floppy\" \"Rarely seen\" \ - \"But sometimes still used\" [] {} 5000".into(), - description: "Show a notification on the desktop for 5 seconds".into(), - result: None - }, - ]), - PluginSignature::build("dbus get") - .dbus_command() - .accepts_dbus_client_options() - .accepts_timeout() - .usage("Get a D-Bus property") - .input_output_type(Type::Nothing, Type::Any) - .required_named("dest", SyntaxShape::String, - "The name of the connection to read the property from", - None) - .required("object", SyntaxShape::String, - "The path to the object to read the property from") - .required("interface", SyntaxShape::String, - "The name of the interface the property belongs to") - .required("property", SyntaxShape::String, - "The name of the property to read") - .plugin_examples(vec![ - PluginExample { - example: "dbus get --dest=org.mpris.MediaPlayer2.spotify \ - /org/mpris/MediaPlayer2 \ - org.mpris.MediaPlayer2.Player Metadata".into(), - description: "Get the currently playing song in Spotify".into(), - result: Some(Value::record(nu_protocol::record!( - "xesam:title" => str!("Birdie"), - "xesam:artist" => Value::list(vec![ - str!("LOVE PSYCHEDELICO") - ], Span::unknown()), - "xesam:album" => str!("Love Your Love"), - "xesam:url" => str!("https://open.spotify.com/track/51748BvzeeMs4PIdPuyZmv"), - ), Span::unknown())) - }, - ]), - PluginSignature::build("dbus get-all") - .dbus_command() - .accepts_dbus_client_options() - .accepts_timeout() - .usage("Get all D-Bus properties for the given object") - .input_output_type(Type::Nothing, Type::Record(vec![])) - .required_named("dest", SyntaxShape::String, - "The name of the connection to read the property from", - None) - .required("object", SyntaxShape::String, - "The path to the object to read the property from") - .required("interface", SyntaxShape::String, - "The name of the interface the property belongs to") - .plugin_examples(vec![ - PluginExample { - example: "dbus get-all --dest=org.mpris.MediaPlayer2.spotify \ - /org/mpris/MediaPlayer2 \ - org.mpris.MediaPlayer2.Player".into(), - description: "Get the current player state of Spotify".into(), - result: Some(Value::record(nu_protocol::record!( - "CanPlay" => Value::bool(true, Span::unknown()), - "Volume" => Value::float(0.43, Span::unknown()), - "PlaybackStatus" => str!("Paused"), - ), Span::unknown())) - }, - ]), - PluginSignature::build("dbus set") - .dbus_command() - .accepts_dbus_client_options() - .accepts_timeout() - .usage("Set a D-Bus property") - .input_output_type(Type::Nothing, Type::Nothing) - .named("signature", SyntaxShape::String, - "Signature of the value to set, in D-Bus format.\n \ - If not provided, it will be determined from introspection.\n \ - If --no-introspect is specified and this is not provided, it will \ - be guessed (poorly)", None) - .required_named("dest", SyntaxShape::String, - "The name of the connection to write the property on", - None) - .required("object", SyntaxShape::String, - "The path to the object to write the property on") - .required("interface", SyntaxShape::String, - "The name of the interface the property belongs to") - .required("property", SyntaxShape::String, - "The name of the property to write") - .required("value", SyntaxShape::Any, - "The value to write to the property") - .plugin_examples(vec![ - PluginExample { - example: "dbus set --dest=org.mpris.MediaPlayer2.spotify \ - /org/mpris/MediaPlayer2 org.mpris.MediaPlayer2.Player \ - Volume 0.5".into(), - description: "Set the volume of Spotify to 50%".into(), - result: None, - }, - ]), - PluginSignature::build("dbus list") - .dbus_command() - .accepts_dbus_client_options() - .accepts_timeout() - .usage("List all available connection names on the bus") - .extra_usage("These can be used as arguments for --dest on any of the other commands.") - .input_output_type(Type::Nothing, Type::List(Type::String.into())) - .optional("pattern", SyntaxShape::String, - "An optional glob-like pattern to filter the result by") - .plugin_examples(vec![ - PluginExample { - example: "dbus list".into(), - description: "List all names available on the bus".into(), - result: None, - }, - PluginExample { - example: "dbus list org.freedesktop.*".into(), - description: "List top-level freedesktop.org names on the bus \ - (e.g. matches `org.freedesktop.PowerManagement`, \ - but not `org.freedesktop.Management.Inhibit`)".into(), - result: Some(Value::list(vec![ - str!("org.freedesktop.DBus"), - str!("org.freedesktop.Flatpak"), - str!("org.freedesktop.Notifications"), - ], Span::unknown())), - }, - PluginExample { - example: "dbus list org.mpris.MediaPlayer2.**".into(), - description: "List all MPRIS2 media players on the bus".into(), - result: Some(Value::list(vec![ - str!("org.mpris.MediaPlayer2.spotify"), - str!("org.mpris.MediaPlayer2.kdeconnect.mpris_000001"), - ], Span::unknown())), - }, - ]) + Box::new(commands::Main), + Box::new(commands::Introspect), + Box::new(commands::Call), + Box::new(commands::Get), + Box::new(commands::GetAll), + Box::new(commands::Set), + Box::new(commands::List), ] } - - fn run( - &self, - name: &str, - _engine: &EngineInterface, - call: &EvaluatedCall, - _input: &Value, - ) -> Result { - match name { - "dbus" => Err(LabeledError { - label: "The `dbus` command requires a subcommand".into(), - msg: "add --help to see subcommands".into(), - span: Some(call.head), - }), - - "dbus introspect" => self.introspect(call), - "dbus call" => self.call(call), - "dbus get" => self.get(call), - "dbus get-all" => self.get_all(call), - "dbus set" => self.set(call), - "dbus list" => self.list(call), - - _ => Err(LabeledError { - label: "Plugin invoked with unknown command name".into(), - msg: "unknown command".into(), - span: Some(call.head), - }), - } - } } /// For conveniently adding the base options to a dbus command @@ -274,10 +37,9 @@ trait DbusSignatureUtilExt { fn accepts_timeout(self) -> Self; } -impl DbusSignatureUtilExt for PluginSignature { +impl DbusSignatureUtilExt for nu_protocol::Signature { fn dbus_command(self) -> Self { - self.search_terms(vec!["dbus".into()]) - .category(nu_protocol::Category::Platform) + self.category(nu_protocol::Category::Platform) } fn accepts_dbus_client_options(self) -> Self { @@ -312,86 +74,3 @@ impl DbusSignatureUtilExt for PluginSignature { ) } } - -impl NuPluginDbus { - fn introspect(&self, call: &EvaluatedCall) -> Result { - let config = DbusClientConfig::try_from(call)?; - let dbus = DbusClient::new(config)?; - let node = dbus.introspect(&call.get_flag("dest")?.unwrap(), &call.req(0)?)?; - Ok(node.to_value(call.head)) - } - - fn call(&self, call: &EvaluatedCall) -> Result { - let config = DbusClientConfig::try_from(call)?; - let dbus = DbusClient::new(config)?; - let values = dbus.call( - &call.get_flag("dest")?.unwrap(), - &call.req(0)?, - &call.req(1)?, - &call.req(2)?, - call.get_flag("signature")?.as_ref(), - &call.positional[3..], - )?; - - let flatten = !call.get_flag::("no-flatten")?.unwrap_or(false); - - // Make the output easier to deal with by returning a list only if there are multiple return - // values (not so common) - match values.len() { - 0 if flatten => Ok(Value::nothing(call.head)), - 1 if flatten => Ok(values.into_iter().nth(0).unwrap()), - _ => Ok(Value::list(values, call.head)), - } - } - - fn get(&self, call: &EvaluatedCall) -> Result { - let config = DbusClientConfig::try_from(call)?; - let dbus = DbusClient::new(config)?; - dbus.get( - &call.get_flag("dest")?.unwrap(), - &call.req(0)?, - &call.req(1)?, - &call.req(2)?, - ) - } - - fn get_all(&self, call: &EvaluatedCall) -> Result { - let config = DbusClientConfig::try_from(call)?; - let dbus = DbusClient::new(config)?; - dbus.get_all( - &call.get_flag("dest")?.unwrap(), - &call.req(0)?, - &call.req(1)?, - ) - } - - fn set(&self, call: &EvaluatedCall) -> Result { - let config = DbusClientConfig::try_from(call)?; - let dbus = DbusClient::new(config)?; - dbus.set( - &call.get_flag("dest")?.unwrap(), - &call.req(0)?, - &call.req(1)?, - &call.req(2)?, - call.get_flag("signature")?.as_ref(), - &call.req(3)?, - )?; - Ok(Value::nothing(call.head)) - } - - fn list(&self, call: &EvaluatedCall) -> Result { - let config = DbusClientConfig::try_from(call)?; - let dbus = DbusClient::new(config)?; - let pattern = call - .opt::(0)? - .map(|pat| Pattern::new(&pat, Some('.'))); - let result = dbus.list(pattern.as_ref())?; - Ok(Value::list( - result - .into_iter() - .map(|s| Value::string(s, call.head)) - .collect(), - call.head, - )) - } -}