diff --git a/Cargo.toml b/Cargo.toml index 8833015..146e864 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,6 @@ [workspace] -members = ["networkmanager"] +members = [ + "networkmanager", + "timedate", + "upower", +] diff --git a/mpris2/.gitignore b/mpris2/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/mpris2/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/mpris2/Cargo.toml b/mpris2/Cargo.toml new file mode 100644 index 0000000..b6dd581 --- /dev/null +++ b/mpris2/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "mpris2-zbus" +description = "zbus-based bindings for MPRIS2 (Media Player Remote Interfacing Specification) on Linux" +version = "0.1.0" +edition = "2021" +license = "MPL-2.0" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +thiserror = "1.0" +time = { version = "0.3", features = ["parsing"] } +zbus = "3.8.0" +zvariant = "3.10" + +[dev-dependencies] +miette = { version = "4.3", features = ["fancy"] } +tokio = { version = "1", features = ["full"] } diff --git a/mpris2/LICENSE.md b/mpris2/LICENSE.md new file mode 100644 index 0000000..c7ed71f --- /dev/null +++ b/mpris2/LICENSE.md @@ -0,0 +1,346 @@ +# Mozilla Public License Version 2.0 + +### 1. Definitions + +**1.1. “Contributor”** + means each individual or legal entity that creates, contributes to +the creation of, or owns Covered Software. + +**1.2. “Contributor Version”** + means the combination of the Contributions of others (if any) used +by a Contributor and that particular Contributor's Contribution. + +**1.3. “Contribution”** + means Covered Software of a particular Contributor. + +**1.4. “Covered Software”** + means Source Code Form to which the initial Contributor has attached +the notice in Exhibit A, the Executable Form of such Source Code +Form, and Modifications of such Source Code Form, in each case +including portions thereof. + +**1.5. “Incompatible With Secondary Licenses”** + means + +- **(a)** that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or +- **(b)** that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +**1.6. “Executable Form”** + means any form of the work other than Source Code Form. + +**1.7. “Larger Work”** + means a work that combines Covered Software with other material, in +a separate file or files, that is not Covered Software. + +**1.8. “License”** + means this document. + +**1.9. “Licensable”** + means having the right to grant, to the maximum extent possible, +whether at the time of the initial grant or subsequently, any and +all of the rights conveyed by this License. + +**1.10. “Modifications”** + means any of the following: + +- **(a)** any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or +- **(b)** any new file in Source Code Form that contains any Covered + Software. + +**1.11. “Patent Claims” of a Contributor** + means any patent claim(s), including without limitation, method, +process, and apparatus claims, in any patent Licensable by such +Contributor that would be infringed, but for the grant of the +License, by the making, using, selling, offering for sale, having +made, import, or transfer of either its Contributions or its +Contributor Version. + +**1.12. “Secondary License”** + means either the GNU General Public License, Version 2.0, the GNU +Lesser General Public License, Version 2.1, the GNU Affero General +Public License, Version 3.0, or any later versions of those +licenses. + +**1.13. “Source Code Form”** + means the form of the work preferred for making modifications. + +**1.14. “You” (or “Your”)** + means an individual or a legal entity exercising rights under this +License. For legal entities, “You” includes any entity that +controls, is controlled by, or is under common control with You. For +purposes of this definition, “control” means **(a)** the power, direct +or indirect, to cause the direction or management of such entity, +whether by contract or otherwise, or **(b)** ownership of more than +fifty percent (50%) of the outstanding shares or beneficial +ownership of such entity. + +### 2. License Grants and Conditions + +#### 2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +- **(a)** under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and +- **(b)** under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +#### 2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +#### 2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +- **(a)** for any code that a Contributor has removed from Covered Software; + or +- **(b)** for infringements caused by: **(i)** Your and any other third party's + modifications of Covered Software, or **(ii)** the combination of its + Contributions with other software (except as part of its Contributor + Version); or +- **(c)** under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +#### 2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +#### 2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +#### 2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +#### 2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +### 3. Responsibilities + +#### 3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +#### 3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +- **(a)** such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +- **(b)** You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +#### 3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +#### 3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +#### 3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +### 4. Inability to Comply Due to Statute or Regulation + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: **(a)** comply with +the terms of this License to the maximum extent possible; and **(b)** +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +### 5. Termination + +**5.1.** The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated **(a)** provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and **(b)** on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +**5.2.** If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +**5.3.** In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +### 6. Disclaimer of Warranty + +> Covered Software is provided under this License on an “as is” +> basis, without warranty of any kind, either expressed, implied, or +> statutory, including, without limitation, warranties that the +> Covered Software is free of defects, merchantable, fit for a +> particular purpose or non-infringing. The entire risk as to the +> quality and performance of the Covered Software is with You. +> Should any Covered Software prove defective in any respect, You +> (not any Contributor) assume the cost of any necessary servicing, +> repair, or correction. This disclaimer of warranty constitutes an +> essential part of this License. No use of any Covered Software is +> authorized under this License except under this disclaimer. + +### 7. Limitation of Liability + +> Under no circumstances and under no legal theory, whether tort +> (including negligence), contract, or otherwise, shall any +> Contributor, or anyone who distributes Covered Software as +> permitted above, be liable to You for any direct, indirect, +> special, incidental, or consequential damages of any character +> including, without limitation, damages for lost profits, loss of +> goodwill, work stoppage, computer failure or malfunction, or any +> and all other commercial damages or losses, even if such party +> shall have been informed of the possibility of such damages. This +> limitation of liability shall not apply to liability for death or +> personal injury resulting from such party's negligence to the +> extent applicable law prohibits such limitation. Some +> jurisdictions do not allow the exclusion or limitation of +> incidental or consequential damages, so this exclusion and +> limitation may not apply to You. + +### 8. Litigation + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +### 9. Miscellaneous + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +### 10. Versions of the License + +#### 10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +#### 10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +#### 10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +#### 10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +## Exhibit A - Source Code Form License Notice + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +## Exhibit B - “Incompatible With Secondary Licenses” Notice + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/mpris2/README.md b/mpris2/README.md new file mode 100644 index 0000000..ed726fc --- /dev/null +++ b/mpris2/README.md @@ -0,0 +1,3 @@ +# mpris2-zbus + +A zbus client proxy for [org.mpris.MediaPlayer2](https://mpris2.readthedocs.io/en/latest/). \ No newline at end of file diff --git a/mpris2/examples/list.rs b/mpris2/examples/list.rs new file mode 100644 index 0000000..778ef83 --- /dev/null +++ b/mpris2/examples/list.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MPL-2.0 +use miette::{IntoDiagnostic, Result, WrapErr}; +use mpris2_zbus::{media_player::MediaPlayer, playlists::ordering::PlaylistOrdering}; +use zbus::Connection; + +#[tokio::main] +async fn main() -> Result<()> { + let connection = Connection::session() + .await + .into_diagnostic() + .wrap_err("Failed to establish session D-Bus connection")?; + let media_players = MediaPlayer::new_all(&connection) + .await + .into_diagnostic() + .wrap_err("Failed get available players")?; + for media_player in media_players { + let name = media_player + .identity() + .await + .into_diagnostic() + .wrap_err("Failed to get identity for media player")?; + let desktop_entry = media_player + .desktop_entry() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get desktop entry for media player '{}'", name))?; + println!("{} ({})", name, desktop_entry); + let player = media_player + .player() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get player for media player '{}'", name))?; + let playback_status = player + .playback_status() + .await + .into_diagnostic() + .wrap_err_with(|| { + format!("Failed to get playback status for media player '{}'", name) + })?; + println!("\tPlayback Status: {}", playback_status); + let position = player + .position() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get position for media player '{}'", name))? + .map(|s| format!("{} seconds", s.as_seconds_f32())) + .unwrap_or_else(|| "N/A".to_owned()); + println!("\tPosition: {}", position); + if !player + .can_seek() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get can_seek for media player '{}'", name))? + { + println!("\tDoesn't support seeking"); + } else { + println!("\tSupports seeking"); + } + let supported_rates = player + .available_rates() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get supported rates for media player '{}'", name))? + .map(|s| format!("{}x through {}x", s.start(), s.end())) + .unwrap_or_else(|| "N/A".to_owned()); + println!("\tSupported Rates: {}", supported_rates); + let current_rate = player + .rate() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get current rate for media player '{}'", name))? + .map(|s| format!("{}x", s)) + .unwrap_or_else(|| "N/A".to_owned()); + println!("\tCurrent Rate: {}", current_rate); + let metadata = player + .metadata() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get metadata for media player '{}'", name))?; + println!("\tMetadata: {}", metadata); + let track_list = media_player + .track_list() + .await + .into_diagnostic() + .wrap_err_with(|| format!("Failed to get track list for media player '{}'", name))?; + if let Some(track_list) = track_list { + let tracks = track_list + .detailed_tracks() + .await + .into_diagnostic() + .wrap_err_with(|| { + format!("Failed to get detailed tracks for media player '{}'", name) + })?; + println!("\tTracks:"); + for (track, metadata) in tracks { + println!("\t\t{}", track); + if let Some(title) = metadata.title() { + println!("\t\t\tTitle: {}", title); + } + if let Some(album) = metadata.album() { + println!("\t\t\tAlbum: {}", album); + } + if let Some(artists) = metadata.artists() { + println!("\t\t\tArtists: {}", artists.join(", ")); + } + if let Some(composers) = metadata.composer() { + println!("\t\t\tComposers: {}", composers.join(", ")); + } + if let Some(bpm) = metadata.bpm() { + println!("\t\t\tBPM: {}", bpm); + } + } + } else { + println!("\tTracks: N/A"); + } + let playlists = media_player + .playlists() + .await + .into_diagnostic() + .wrap_err_with(|| { + format!( + "Failed to get playlists interface for media player '{}'", + name + ) + })?; + if let Some(playlists) = playlists { + println!("\tPlaylists:"); + let playlist_count = playlists + .playlist_count() + .await + .into_diagnostic() + .wrap_err_with(|| { + format!("Failed to get playlist count for media player '{}'", name) + })?; + if playlist_count > 0 { + let playlists = playlists + .get_playlists(0, playlist_count, PlaylistOrdering::Alphabetical, false) + .await + .into_diagnostic() + .wrap_err_with(|| { + format!("Failed to get playlists for media player '{}'", name) + })?; + for playlist in playlists { + println!("\t{}", playlist.id()); + println!("\t\tName: {}", playlist.name()); + println!("\t\tIcon: {}", playlist.icon()); + } + } else { + println!("\t\t None :("); + } + } else { + println!("\tPlaylists: N/A"); + } + } + Ok(()) +} diff --git a/mpris2/src/bindings.rs b/mpris2/src/bindings.rs new file mode 100644 index 0000000..39468ea --- /dev/null +++ b/mpris2/src/bindings.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: MPL-2.0 +pub mod media_player; +pub mod player; +pub mod playlist; +pub mod track_list; diff --git a/mpris2/src/bindings/media_player.rs b/mpris2/src/bindings/media_player.rs new file mode 100644 index 0000000..33f7c5f --- /dev/null +++ b/mpris2/src/bindings/media_player.rs @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MPL-2.0 +//! # DBus interface proxies for: `org.mpris.MediaPlayer2` +//! +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.firefox.instance103520' on session bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2", + default_path = "/org/mpris/MediaPlayer2" +)] +trait MediaPlayer2 { + /// Quit method + fn quit(&self) -> zbus::Result<()>; + + /// Raise method + fn raise(&self) -> zbus::Result<()>; + + /// CanQuit property + #[dbus_proxy(property)] + fn can_quit(&self) -> zbus::Result; + + /// CanRaise property + #[dbus_proxy(property)] + fn can_raise(&self) -> zbus::Result; + + /// DesktopEntry property + #[dbus_proxy(property)] + fn desktop_entry(&self) -> zbus::Result; + + /// HasTrackList property + #[dbus_proxy(property)] + fn has_track_list(&self) -> zbus::Result; + + /// Identity property + #[dbus_proxy(property)] + fn identity(&self) -> zbus::Result; + + /// SupportedMimeTypes property + #[dbus_proxy(property)] + fn supported_mime_types(&self) -> zbus::Result>; + + /// SupportedUriSchemes property + #[dbus_proxy(property)] + fn supported_uri_schemes(&self) -> zbus::Result>; +} diff --git a/mpris2/src/bindings/player.rs b/mpris2/src/bindings/player.rs new file mode 100644 index 0000000..9f2ae11 --- /dev/null +++ b/mpris2/src/bindings/player.rs @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: MPL-2.0 +//! # DBus interface proxies for: `org.mpris.MediaPlayer2.Player` +//! +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.firefox.instance103520' on session bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::PropertiesProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PeerProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +use crate::track::TrackId; +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2.Player", + default_path = "/org/mpris/MediaPlayer2" +)] +trait Player { + /// Next method + fn next(&self) -> zbus::Result<()>; + + /// OpenUri method + fn open_uri(&self, uri: &str) -> zbus::Result<()>; + + /// Pause method + fn pause(&self) -> zbus::Result<()>; + + /// Play method + fn play(&self) -> zbus::Result<()>; + + /// PlayPause method + fn play_pause(&self) -> zbus::Result<()>; + + /// Previous method + fn previous(&self) -> zbus::Result<()>; + + /// Seek method + fn seek(&self, offset: i64) -> zbus::Result<()>; + + /// SetPosition method + fn set_position(&self, track_id: &TrackId, position: i64) -> zbus::Result<()>; + + /// Stop method + fn stop(&self) -> zbus::Result<()>; + + /// Seeked signal + #[dbus_proxy(signal)] + fn seeked(&self, position: i64) -> zbus::Result<()>; + + /// CanControl property + #[dbus_proxy(property)] + fn can_control(&self) -> zbus::Result; + + /// CanGoNext property + #[dbus_proxy(property)] + fn can_go_next(&self) -> zbus::Result; + + /// CanGoPrevious property + #[dbus_proxy(property)] + fn can_go_previous(&self) -> zbus::Result; + + /// CanPause property + #[dbus_proxy(property)] + fn can_pause(&self) -> zbus::Result; + + /// CanPlay property + #[dbus_proxy(property)] + fn can_play(&self) -> zbus::Result; + + /// CanSeek property + #[dbus_proxy(property)] + fn can_seek(&self) -> zbus::Result; + + /// MaximumRate property + #[dbus_proxy(property)] + fn maximum_rate(&self) -> zbus::Result; + + /// Metadata property + #[dbus_proxy(property)] + fn metadata( + &self, + ) -> zbus::Result>; + + /// MinimumRate property + #[dbus_proxy(property)] + fn minimum_rate(&self) -> zbus::Result; + + /// PlaybackStatus property + #[dbus_proxy(property)] + fn playback_status(&self) -> zbus::Result; + + /// Position property + #[dbus_proxy(property)] + fn position(&self) -> zbus::Result; + + /// Rate property + #[dbus_proxy(property)] + fn rate(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_rate(&self, value: f64) -> zbus::Result<()>; + + /// Shuffle property (optional) + #[dbus_proxy(property)] + fn shuffle(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_shuffle(&self, value: bool) -> zbus::Result<()>; + + /// LoopStatus property (optional) + #[dbus_proxy(property)] + fn loop_status(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_loop_status(&self, value: String) -> zbus::Result<()>; + + /// Volume property + #[dbus_proxy(property)] + fn volume(&self) -> zbus::Result; + #[dbus_proxy(property)] + fn set_volume(&self, value: f64) -> zbus::Result<()>; +} diff --git a/mpris2/src/bindings/playlist.rs b/mpris2/src/bindings/playlist.rs new file mode 100644 index 0000000..da667f9 --- /dev/null +++ b/mpris2/src/bindings/playlist.rs @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: MPL-2.0 +//! # DBus interface proxies for: `org.mpris.MediaPlayer2`, `org.mpris.MediaPlayer2.Player`, `org.mpris.MediaPlayer2.TrackList`, `org.mpris.MediaPlayer2.Playlists` +//! +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.org.gnome.Music' on session bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +use crate::playlists::{id::PlaylistId, ordering::PlaylistOrdering, playlist::Playlist}; +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2.Playlists", + default_path = "/org/mpris/MediaPlayer2" +)] +trait Playlists { + /// ActivatePlaylist method + fn activate_playlist(&self, playlist_id: &PlaylistId) -> zbus::Result<()>; + + /// GetPlaylists method + fn get_playlists( + &self, + index: u32, + max_count: u32, + order: PlaylistOrdering, + reverse_order: bool, + ) -> zbus::Result>; + + /// PlaylistChanged signal + #[dbus_proxy(signal)] + fn playlist_changed(&self, playlist: Playlist) -> zbus::Result<()>; + + /// ActivePlaylist property + #[dbus_proxy(property)] + fn active_playlist(&self) -> zbus::Result<(bool, Playlist)>; + + /// Orderings property + #[dbus_proxy(property)] + fn orderings(&self) -> zbus::Result>; + + /// PlaylistCount property + #[dbus_proxy(property)] + fn playlist_count(&self) -> zbus::Result; +} diff --git a/mpris2/src/bindings/track_list.rs b/mpris2/src/bindings/track_list.rs new file mode 100644 index 0000000..6b916e7 --- /dev/null +++ b/mpris2/src/bindings/track_list.rs @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MPL-2.0 +//! # DBus interface proxies for: `org.mpris.MediaPlayer2`, `org.mpris.MediaPlayer2.Player`, `org.mpris.MediaPlayer2.TrackList`, `org.mpris.MediaPlayer2.Playlists` +//! +//! This code was generated by `zbus-xmlgen` `2.0.1` from DBus introspection data. +//! Source: `Interface '/org/mpris/MediaPlayer2' from service 'org.mpris.MediaPlayer2.org.gnome.Music' on session bus`. +//! +//! You may prefer to adapt it, instead of using it verbatim. +//! +//! More information can be found in the +//! [Writing a client proxy](https://dbus.pages.freedesktop.org/zbus/client.html) +//! section of the zbus documentation. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! +//! …consequently `zbus-xmlgen` did not generate code for the above interfaces. + +use crate::track::TrackId; +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.mpris.MediaPlayer2.TrackList", + default_path = "/org/mpris/MediaPlayer2" +)] +trait TrackList { + /// AddTrack method + fn add_track(&self, uri: &str, after_track: &TrackId, set_as_current: bool) + -> zbus::Result<()>; + + /// GetTracksMetadata method + fn get_tracks_metadata( + &self, + track_ids: Vec, + ) -> zbus::Result>>; + + /// GoTo method + fn go_to(&self, track_id: &TrackId) -> zbus::Result<()>; + + /// RemoveTrack method + fn remove_track(&self, track_id: &TrackId) -> zbus::Result<()>; + + /// TrackListReplaced signal + #[dbus_proxy(signal)] + fn track_list_replaced(&self, tracks: Vec, current_track: TrackId) + -> zbus::Result<()>; + + /// CanEditTracks property + #[dbus_proxy(property)] + fn can_edit_tracks(&self) -> zbus::Result; + + /// Tracks property + #[dbus_proxy(property)] + fn tracks(&self) -> zbus::Result>; +} diff --git a/mpris2/src/error.rs b/mpris2/src/error.rs new file mode 100644 index 0000000..b5e92aa --- /dev/null +++ b/mpris2/src/error.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MPL-2.0 + +#[derive(Debug, thiserror::Error)] +pub enum Error { + /// Invalid enum variant when converting from String. + #[error("Invalid enum variant: {got}, expected something in {expected:?}")] + InvalidEnum { + got: String, + expected: &'static [&'static str], + }, + + #[error("Tried to extract a {wanted}, but it was actually {actual}")] + IncorrectVariant { + wanted: &'static str, + actual: &'static str, + }, + + #[error("Tried to convert Value::{wanted}, but it was got {actual:?}")] + IncorrectValue { + wanted: &'static str, + actual: zvariant::OwnedValue, + }, + + /// A zbus error. + #[error("zbus error: {0}")] + Zbus(zbus::Error), + + /// A zbus::fdo error. + #[error("zbus fdo error: {0}")] + Fdo(zbus::fdo::Error), +} + +impl From for Error { + fn from(err: zbus::fdo::Error) -> Self { + match err { + zbus::fdo::Error::ZBus(err) => Self::Zbus(err), + _ => Self::Fdo(err), + } + } +} + +impl From for Error { + fn from(err: zbus::Error) -> Self { + match err { + zbus::Error::FDO(err) => Self::Fdo(*err), + _ => Self::Zbus(err), + } + } +} + +pub type Result = std::result::Result; diff --git a/mpris2/src/lib.rs b/mpris2/src/lib.rs new file mode 100644 index 0000000..d646405 --- /dev/null +++ b/mpris2/src/lib.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MPL-2.0 +pub mod bindings; +pub mod error; +pub mod media_player; +pub mod metadata; +pub mod player; +pub mod playlists; +pub mod track; +pub mod track_list; + +pub(crate) fn handle_optional(input: zbus::Result) -> error::Result> { + match input { + Ok(input) => Ok(Some(input)), + Err(zbus::Error::FDO(fdo_error)) + if matches!(*fdo_error, zbus::fdo::Error::NotSupported(_)) => + { + Ok(None) + } + Err(err) => Err(error::Error::from(err)), + } +} diff --git a/mpris2/src/media_player.rs b/mpris2/src/media_player.rs new file mode 100644 index 0000000..def4d3d --- /dev/null +++ b/mpris2/src/media_player.rs @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: MPL-2.0 +use crate::{ + bindings::{ + media_player::MediaPlayer2Proxy, player::PlayerProxy, playlist::PlaylistsProxy, + track_list::TrackListProxy, + }, + error::{Error, Result}, + player::Player, + playlists::Playlists, + track_list::TrackList, +}; +use std::ops::Deref; +use zbus::{fdo::DBusProxy, names::OwnedBusName, Connection}; + +#[derive(Debug, Clone)] +pub struct MediaPlayer { + proxy: MediaPlayer2Proxy<'static>, +} + +impl MediaPlayer { + /// Creates a new instance of the `org.mpris.MediaPlayer2` interface. + pub async fn new(connection: &Connection, name: OwnedBusName) -> Result { + MediaPlayer2Proxy::builder(connection) + .destination(name)? + .build() + .await + .map(Self::from) + .map_err(Error::from) + } + + /// Gets the names of all the MPRIS players that are available on the current session. + pub async fn available_players(connection: &Connection) -> Result> { + let dbus = DBusProxy::builder(connection) + .path("/org/freedesktop/DBus")? + .build() + .await?; + let mut players = Vec::new(); + for name in dbus.list_names().await? { + if name.starts_with("org.mpris.MediaPlayer2.") { + players.push(name); + } + } + Ok(players) + } + + /// Gets a new instance of all the MPRIS players that are available on the current session. + pub async fn new_all(connection: &Connection) -> Result> { + let players = Self::available_players(connection).await?; + let mut instances = Vec::with_capacity(players.len()); + for player in players { + instances.push(Self::new(connection, player).await?); + } + Ok(instances) + } + + /// Returns an instance to the `org.mpris.MediaPlayer2.Player` interface of this object. + pub async fn player(&self) -> Result { + PlayerProxy::builder(self.proxy.connection()) + .destination(self.proxy.destination().to_owned())? + .build() + .await + .map(Player::from) + .map_err(Error::from) + } + + /// Returns an instance to the `org.mpris.MediaPlayer2.TrackList` interface of this object, + /// if a track list is available. + pub async fn track_list(&self) -> Result> { + if self.proxy.has_track_list().await? { + TrackListProxy::builder(self.proxy.connection()) + .destination(self.proxy.destination().to_owned())? + .build() + .await + .map(TrackList::from) + .map(Some) + .map_err(Error::from) + } else { + Ok(None) + } + } + + /// Returns an instance to the `org.mpris.MediaPlayer2.Playlists` interface of this object, + /// if a track list is available. + pub async fn playlists(&self) -> Result> { + if self.proxy.has_track_list().await? { + PlaylistsProxy::builder(self.proxy.connection()) + .destination(self.proxy.destination().to_owned())? + .build() + .await + .map(Playlists::from) + .map(Some) + .map_err(Error::from) + } else { + Ok(None) + } + } +} + +impl Deref for MediaPlayer { + type Target = MediaPlayer2Proxy<'static>; + + fn deref(&self) -> &Self::Target { + &self.proxy + } +} + +impl From> for MediaPlayer { + fn from(proxy: MediaPlayer2Proxy<'static>) -> Self { + Self { proxy } + } +} diff --git a/mpris2/src/metadata.rs b/mpris2/src/metadata.rs new file mode 100644 index 0000000..44dadd3 --- /dev/null +++ b/mpris2/src/metadata.rs @@ -0,0 +1,533 @@ +// SPDX-License-Identifier: MPL-2.0 +use crate::error::{Error, Result}; +use std::{ + collections::HashMap, + fmt, + ops::{Deref, DerefMut}, +}; +use time::{Duration, OffsetDateTime}; +use zbus::zvariant::{OwnedObjectPath, Value as ZValue}; + +#[derive(Debug, Clone, PartialEq)] +pub struct Metadata { + inner: HashMap, +} + +impl Metadata { + /// `xesam:album`: The track artist(s). + pub fn album(&self) -> Option { + self.inner + .get("xesam:album") + .cloned() + .and_then(|v| v.try_into_string().ok()) + } + + /// `xesam:artist`: The track artist(s). + pub fn artists(&self) -> Option> { + self.inner + .get("xesam:artist") + .cloned() + .and_then(|artists| artists.try_into_array().ok()) + .map(|artists| { + artists + .into_iter() + .filter_map(|v| v.try_into_string().ok()) + .collect() + }) + } + + /// `xesam:asText`: The track lyrics. + pub fn lyrics(&self) -> Option { + self.inner + .get("xesam:asText") + .cloned() + .and_then(|v| v.try_into_string().ok()) + } + + /// `xesam:albumArtist`: The album artist(s). + pub fn album_artists(&self) -> Option> { + self.inner + .get("xesam:albumArtist") + .cloned() + .and_then(|artists| artists.try_into_array().ok()) + .map(|artists| { + artists + .into_iter() + .filter_map(|v| v.try_into_string().ok()) + .collect() + }) + } + + /// `xesam:audioBPM`: The speed of the music, in beats per minute. + pub fn bpm(&self) -> Option { + self.inner + .get("xesam:audioBPM") + .cloned() + .and_then(|v| v.try_into_uint().ok()) + } + + /// `xesam:autoRating`: An automatically-generated rating, based on things such as how often it has been played. + /// This should be in the range 0.0 to 1.0. + pub fn auto_rating(&self) -> Option { + self.inner + .get("xesam:autoRating") + .cloned() + .and_then(|v| v.try_into_double().ok()) + } + + /// `xesam:composer`: The composer(s) of the track. + pub fn composer(&self) -> Option> { + self.inner + .get("xesam:composer") + .cloned() + .and_then(|artists| artists.try_into_array().ok()) + .map(|artists| { + artists + .into_iter() + .filter_map(|v| v.try_into_string().ok()) + .collect() + }) + } + + /// `xesam:contentCreated`: When the track was created. Usually only the year component will be useful. + pub fn created(&self) -> Option { + self.inner + .get("xesam:contentCreated") + .cloned() + .and_then(|v| v.try_into_date().ok()) + } + + /// `xesam:discNumber`: The disc number on the album that this track is from. + pub fn disc_number(&self) -> Option { + self.inner + .get("xesam:discNumber") + .cloned() + .and_then(|v| v.try_into_uint().ok()) + } + + /// `xesam:firstUsed`: When the track was first played. + pub fn first_played(&self) -> Option { + self.inner + .get("xesam:firstUsed") + .cloned() + .and_then(|v| v.try_into_date().ok()) + } + + /// `xesam:genre`: The genre(s) of the track. + pub fn genre(&self) -> Option> { + self.inner + .get("xesam:genre") + .cloned() + .and_then(|artists| artists.try_into_array().ok()) + .map(|artists| { + artists + .into_iter() + .filter_map(|v| v.try_into_string().ok()) + .collect() + }) + } + + /// `xesam:lastUsed`: When the track was last played. + pub fn last_played(&self) -> Option { + self.inner + .get("xesam:lastUsed") + .cloned() + .and_then(|v| v.try_into_date().ok()) + } + + /// `xesam:lyricist`: The lyricist(s) of the track. + pub fn lyricist(&self) -> Option> { + self.inner + .get("xesam:lyricist") + .cloned() + .and_then(|artists| artists.try_into_array().ok()) + .map(|artists| { + artists + .into_iter() + .filter_map(|v| v.try_into_string().ok()) + .collect() + }) + } + + /// `xesam:title`: The track title. + pub fn title(&self) -> Option { + self.inner + .get("xesam:title") + .cloned() + .and_then(|v| v.try_into_string().ok()) + } + + /// `xesam:trackNumber`: The track number on the album that this track is from. + pub fn track_number(&self) -> Option { + self.inner + .get("xesam:trackNumber") + .cloned() + .and_then(|v| v.try_into_uint().ok()) + } + + /// `xesam:url`: The location of the media file. + pub fn url(&self) -> Option { + self.inner + .get("xesam:url") + .cloned() + .and_then(|v| v.try_into_string().ok()) + } + + /// `xesam:useCount`: The number of times the track has been played. + pub fn use_count(&self) -> Option { + self.inner + .get("xesam:useCount") + .cloned() + .and_then(|v| v.try_into_uint().ok()) + } + + /// `xesam:userRating`: The user's rating of the track. + pub fn user_rating(&self) -> Option { + self.inner + .get("xesam:userRating") + .cloned() + .and_then(|v| v.try_into_double().ok()) + } + + /// `mpris:trackid`: D-Bus path: A unique identity for this track within the context of an MPRIS object (eg: tracklist). + pub fn track_id(&self) -> Option { + self.inner + .get("mpris:trackid") + .cloned() + .and_then(|v| v.try_into_string().ok()) + .and_then(|path| OwnedObjectPath::try_from(path).ok()) + } + + /// `mpris:length`: The length of the track in microseconds. + pub fn length(&self) -> Option { + self.inner + .get("mpris:length") + .cloned() + .and_then(|v| match &v { + MetadataValue::Int(i) => Some(*i), + MetadataValue::UInt(u) => Some(*u as i64), + MetadataValue::Str(s) => s.parse().ok(), + _ => None, + }) + .map(Duration::microseconds) + } + + /// `mpris:artUrl`: The location of an image representing the track or album. + /// Clients should not assume this will continue to exist when the media player stops giving out the URL. + pub fn art_url(&self) -> Option { + self.inner + .get("mpris:artUrl") + .cloned() + .and_then(|v| v.try_into_string().ok()) + } +} + +impl Deref for Metadata { + type Target = HashMap; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl DerefMut for Metadata { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } +} + +impl fmt::Display for Metadata { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{{")?; + let mut iter = self.inner.iter().peekable(); + while let Some((k, v)) = iter.next() { + if iter.peek().is_some() { + write!(f, "{}: {}, ", k, v)?; + } else { + write!(f, "{}: {}", k, v)?; + } + } + write!(f, "}}") + } +} + +impl<'a, V: Into>> From> for Metadata { + fn from(map: HashMap) -> Self { + Self { + inner: map + .into_iter() + .map(|(k, v)| (k, MetadataValue::from(&v.into()))) + .collect(), + } + } +} + +#[derive(Clone, PartialEq)] +pub enum MetadataValue { + Str(String), + Double(f64), + Int(i64), + UInt(u64), + Bool(bool), + Array(Vec), + Dict(HashMap), + __Unsupported, +} + +impl MetadataValue { + fn variant(&self) -> &'static str { + match self { + MetadataValue::Str(_) => "Str", + MetadataValue::Double(_) => "Double", + MetadataValue::Int(_) => "Int", + MetadataValue::UInt(_) => "UInt", + MetadataValue::Bool(_) => "Bool", + MetadataValue::Array(_) => "Array", + MetadataValue::Dict(_) => "Dict", + MetadataValue::__Unsupported => "Unsupported", + } + } + + /// Tries to extract a string from the variant, + /// returning an error if the variant is not a string. + pub fn try_into_string(self) -> Result { + match self { + MetadataValue::Str(s) => Ok(s), + _ => Err(Error::IncorrectVariant { + wanted: "Str", + actual: self.variant(), + }), + } + } + + /// Tries to extract a string from the variant, + /// panicking if the variant is not a string. + pub fn into_string(self) -> String { + self.try_into_string() + .unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract a date/time from the variant, + /// returning an error if the variant is not a date/time. + pub fn try_into_date(self) -> Result { + let variant = self.variant(); + match self { + MetadataValue::Str(s) => { + OffsetDateTime::parse(&s, &time::format_description::well_known::Rfc3339).map_err( + |_| Error::IncorrectVariant { + wanted: "String (DateTime)", + actual: variant, + }, + ) + } + _ => Err(Error::IncorrectVariant { + wanted: "String (DateTime)", + actual: variant, + }), + } + } + + /// Tries to extract a date/time from the variant, + /// panicking if the variant is not a date/time. + pub fn into_date(self) -> OffsetDateTime { + self.try_into_date().unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract a double from the variant, + /// returning an error if the variant is not a double. + pub fn try_into_double(self) -> Result { + match self { + MetadataValue::Double(d) => Ok(d), + _ => Err(Error::IncorrectVariant { + wanted: "Double", + actual: self.variant(), + }), + } + } + + /// Tries to extract a double from the variant, + /// panicking if the variant is not a double. + pub fn into_double(self) -> f64 { + self.try_into_double() + .unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract an integer from the variant, + /// returning an error if the variant is not an integer. + pub fn try_into_int(self) -> Result { + match self { + MetadataValue::Int(i) => Ok(i), + _ => Err(Error::IncorrectVariant { + wanted: "Int", + actual: self.variant(), + }), + } + } + + /// Tries to extract an integer from the variant, + /// panicking if the variant is not an integer. + pub fn into_int(self) -> i64 { + self.try_into_int().unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract an unsigned integer from the variant, + /// returning an error if the variant is not an unsigned integer. + pub fn try_into_uint(self) -> Result { + match self { + MetadataValue::UInt(u) => Ok(u), + _ => Err(Error::IncorrectVariant { + wanted: "UInt", + actual: self.variant(), + }), + } + } + + /// Tries to extract an unsigned integer from the variant, + /// panicking if the variant is not an unsigned integer. + pub fn into_uint(self) -> u64 { + self.try_into_uint().unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract a boolean from the variant, + /// returning an error if the variant is not a boolean. + pub fn try_into_bool(self) -> Result { + match self { + MetadataValue::Bool(b) => Ok(b), + _ => Err(Error::IncorrectVariant { + wanted: "Bool", + actual: self.variant(), + }), + } + } + + /// Tries to extract a boolean from the variant, + /// panicking if the variant is not a boolean. + pub fn into_bool(self) -> bool { + self.try_into_bool().unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract an array from the variant, + /// returning an error if the variant is not an array. + pub fn try_into_array(self) -> Result> { + match self { + MetadataValue::Array(a) => Ok(a), + _ => Err(Error::IncorrectVariant { + wanted: "Array", + actual: self.variant(), + }), + } + } + + /// Tries to extract an array from the variant, + /// panicking if the variant is not an array. + pub fn into_array(self) -> Vec { + self.try_into_array() + .unwrap_or_else(|err| panic!("{}", err)) + } + + /// Tries to extract a dictionary from the variant, + /// returning an error if the variant is not a dictionary. + /// The dictionary is returned as a map from string keys to values. + pub fn try_into_dict(self) -> Result> { + match self { + MetadataValue::Dict(d) => Ok(d), + _ => Err(Error::IncorrectVariant { + wanted: "Dict", + actual: self.variant(), + }), + } + } + + /// Tries to extract a dictionary from the variant, + /// panicking if the variant is not a dictionary. + /// The dictionary is returned as a map from string keys to values. + pub fn into_dict(self) -> HashMap { + self.try_into_dict().unwrap_or_else(|err| panic!("{}", err)) + } +} + +impl<'a> From<&ZValue<'a>> for MetadataValue { + fn from(value: &ZValue) -> Self { + match value { + ZValue::U8(u) => Self::UInt(*u as u64), + ZValue::Bool(b) => Self::Bool(*b), + ZValue::I16(i) => Self::Int(*i as i64), + ZValue::U16(u) => Self::UInt(*u as u64), + ZValue::I32(i) => Self::Int(*i as i64), + ZValue::U32(u) => Self::UInt(*u as u64), + ZValue::I64(i) => Self::Int(*i), + ZValue::U64(u) => Self::UInt(*u), + ZValue::F64(f) => Self::Double(*f), + ZValue::Str(s) => Self::Str(s.to_string()), + ZValue::ObjectPath(path) => Self::Str(path.to_string()), + ZValue::Array(a) => Self::Array(a.iter().map(|v| v.into()).collect()), + ZValue::Dict(d) => Self::Dict( + HashMap::::try_from(d.to_owned()) + .unwrap() + .into_iter() + .map(|(k, v)| (k, (&v).into())) + .collect(), + ), + ZValue::Value(value) => Self::from(&**value), + _ => Self::__Unsupported, + } + } +} + +impl fmt::Debug for MetadataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::__Unsupported => write!(f, "__Unsupported"), + Self::Int(i) => write!(f, "{}", i), + Self::UInt(u) => write!(f, "{}", u), + Self::Double(d) => write!(f, "{}", d), + Self::Str(s) => write!(f, "{}", s), + Self::Bool(b) => write!(f, "{}", b), + Self::Array(a) => write!(f, "{:?}", a), + Self::Dict(d) => { + let mut debug_struct = f.debug_struct("Dict"); + for (k, v) in d { + debug_struct.field(k, &v); + } + debug_struct.finish() + } + } + } +} + +impl fmt::Display for MetadataValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::__Unsupported => write!(f, "__Unsupported"), + Self::Int(i) => write!(f, "{}", i), + Self::UInt(u) => write!(f, "{}", u), + Self::Double(d) => write!(f, "{}", d), + Self::Str(s) => write!(f, "\"{}\"", s), + Self::Bool(b) => write!(f, "{}", b), + Self::Array(a) => { + write!(f, "[")?; + let mut iter = a.iter().peekable(); + while let Some(value) = iter.next() { + if iter.peek().is_some() { + write!(f, "{}, ", value)?; + } else { + write!(f, "{}", value)?; + } + } + write!(f, "]") + } + Self::Dict(d) => { + write!(f, "{{")?; + let mut iter = d.iter().peekable(); + while let Some((k, v)) = iter.next() { + if iter.peek().is_some() { + write!(f, "{}: {}, ", k, v)?; + } else { + write!(f, "{}: {}", k, v)?; + } + } + write!(f, "}}") + } + } + } +} diff --git a/mpris2/src/player.rs b/mpris2/src/player.rs new file mode 100644 index 0000000..4ed4c7d --- /dev/null +++ b/mpris2/src/player.rs @@ -0,0 +1,267 @@ +// SPDX-License-Identifier: MPL-2.0 +use crate::{ + bindings::{media_player::MediaPlayer2Proxy, player::PlayerProxy}, + error::{Error, Result}, + handle_optional, + media_player::MediaPlayer, + metadata::Metadata, + track::TrackId, +}; +use std::{ + fmt::{self, Display}, + ops::Deref, + str::FromStr, +}; +use time::Duration; +use zbus::{names::OwnedBusName, Connection}; + +#[derive(Debug, Clone)] +pub struct Player { + proxy: PlayerProxy<'static>, +} + +impl Player { + /// Creates a new instance of the `org.mpris.MediaPlayer2.Player` interface. + pub async fn new(connection: &Connection, name: OwnedBusName) -> Result { + PlayerProxy::builder(connection) + .destination(name)? + .build() + .await + .map(Self::from) + .map_err(Error::from) + } + + /// Returns this player's `org.mpris.MediaPlayer2` instance + pub async fn media_player(&self) -> Result { + let proxy = MediaPlayer2Proxy::builder(self.proxy.connection()) + .destination(self.proxy.destination().to_owned())? + .build() + .await?; + Ok(proxy.into()) + } + + /// Seeks the specified duration. + pub async fn seek(&self, duration: Duration) -> Result { + if self.proxy.can_seek().await? { + self.proxy + .seek(duration.whole_microseconds() as i64) + .await?; + Ok(true) + } else { + Ok(false) + } + } + + /// Sets the current track position. + /// + /// If `track` does not match the id of the currently-playing track, the call is ignored as "stale". + pub async fn set_position(&self, track: &TrackId, position: Duration) -> Result<()> { + self.proxy + .set_position(track, position.whole_microseconds() as i64) + .await + .map_err(Error::from) + } + + /// How far into the current track the player is. + /// + /// Not all players support this, and it will return None if this is the case. + pub async fn position(&self) -> Result> { + handle_optional(self.proxy.position().await.map(Duration::microseconds)) + } + + /// Gets the current playback status of the player. + pub async fn playback_status(&self) -> Result { + self.proxy + .playback_status() + .await + .map_err(Error::from) + .and_then(|status| PlaybackStatus::from_str(&status)) + } + + /// Returns the current rate of playback. + /// + /// Not all players support this, and it will return None if this is the case. + pub async fn rate(&self) -> Result> { + handle_optional(self.proxy.rate().await) + } + + /// Sets the current rate of playback. + pub async fn set_rate(&self, value: f64) -> Result<()> { + handle_optional(self.proxy.set_rate(value).await).map(|_| ()) + } + + /// Returns the minimum supported rate for the player. + /// + /// Not all players support this, and it will return None if this is the case. + pub async fn minimum_rate(&self) -> Result> { + handle_optional(self.proxy.minimum_rate().await) + } + + /// Returns the minimum supported rate for the player. + /// + /// Not all players support this, and it will return None if this is the case. + pub async fn maximum_rate(&self) -> Result> { + handle_optional(self.proxy.maximum_rate().await) + } + + /// Returns the range of playback rates available for the player. + /// + /// Not all players support this, and it will return None if this is the case. + pub async fn available_rates(&self) -> Result>> { + let minimum = match self.minimum_rate().await? { + Some(min) => min, + None => return Ok(None), + }; + let maximum = match self.maximum_rate().await? { + Some(max) => max, + None => return Ok(None), + }; + Ok(Some(minimum..=maximum)) + } + + /// Returns the metadata for the player. + pub async fn metadata(&self) -> Result { + self.proxy + .metadata() + .await + .map(|metadata| metadata.into()) + .map_err(Error::from) + } + + /// Whether the current playlist is shuffled or not. + /// + /// A value of false indicates that playback is progressing linearly through a playlist, + /// while true means playback is progressing through a playlist in some other order. + pub async fn shuffle(&self) -> Result> { + if self.can_control().await? { + handle_optional(self.proxy.shuffle().await) + } else { + Ok(None) + } + } + + /// Set whether the current playlist is shuffled or not. + /// + /// A value of false indicates that playback is progressing linearly through a playlist, + /// while true means playback is progressing through a playlist in some other order. + pub async fn set_shuffle(&self, value: bool) -> Result<()> { + if self.proxy.can_control().await? { + self.proxy.set_shuffle(value).await.map_err(Error::from) + } else { + Ok(()) + } + } + + /// The current loop / repeat status. + pub async fn loop_status(&self) -> Result> { + if self.proxy.can_control().await? { + handle_optional(self.proxy.loop_status().await) + .map(|status| status.and_then(|status| LoopStatus::from_str(&status).ok())) + } else { + Ok(None) + } + } + + /// Set the current loop / repeat status. + pub async fn set_loop_status(&self, value: LoopStatus) -> Result<()> { + if self.proxy.can_control().await? { + handle_optional(self.proxy.set_loop_status(value.to_string()).await).map(|_| ()) + } else { + Ok(()) + } + } +} + +impl Deref for Player { + type Target = PlayerProxy<'static>; + + fn deref(&self) -> &Self::Target { + &self.proxy + } +} + +impl From> for Player { + fn from(proxy: PlayerProxy<'static>) -> Self { + Self { proxy } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PlaybackStatus { + /// A track is currently playing. + Playing, + /// A track is currently paused. + Paused, + /// There is no track currently playing. + Stopped, +} + +impl FromStr for PlaybackStatus { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().trim() { + "playing" => Ok(Self::Playing), + "paused" => Ok(Self::Paused), + "stopped" => Ok(Self::Stopped), + _ => Err(Error::InvalidEnum { + got: s.to_string(), + expected: &["Playing", "Paused", "Stopped"], + }), + } + } +} + +impl Display for PlaybackStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Playing => "Playing", + Self::Paused => "Paused", + Self::Stopped => "Stopped", + } + ) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum LoopStatus { + /// The playback will stop when there are no more tracks to play + None, + /// The current track will start again from the begining once it has finished playing + Track, + /// The playback loops through a list of tracks + Playlist, +} + +impl FromStr for LoopStatus { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().trim() { + "none" => Ok(Self::None), + "track" => Ok(Self::Track), + "playlist" => Ok(Self::Playlist), + _ => Err(Error::InvalidEnum { + got: s.to_string(), + expected: &["Playing", "Paused", "Stopped"], + }), + } + } +} + +impl Display for LoopStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::None => "None", + Self::Track => "Track", + Self::Playlist => "Playlist", + } + ) + } +} diff --git a/mpris2/src/playlists.rs b/mpris2/src/playlists.rs new file mode 100644 index 0000000..4eb6597 --- /dev/null +++ b/mpris2/src/playlists.rs @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MPL-2.0 +pub mod id; +pub mod ordering; +pub mod playlist; + +use crate::{ + bindings::playlist::PlaylistsProxy, + error::{Error, Result}, +}; +use std::ops::Deref; +use zbus::{names::OwnedBusName, Connection}; + +pub struct Playlists { + proxy: PlaylistsProxy<'static>, +} + +impl Playlists { + /// Creates a new instance of the `org.mpris.MediaPlayer2.Playlists` interface. + pub async fn new(connection: &Connection, name: OwnedBusName) -> Result { + PlaylistsProxy::builder(connection) + .destination(name)? + .build() + .await + .map(Self::from) + .map_err(Error::from) + } +} + +impl Deref for Playlists { + type Target = PlaylistsProxy<'static>; + + fn deref(&self) -> &Self::Target { + &self.proxy + } +} + +impl From> for Playlists { + fn from(proxy: PlaylistsProxy<'static>) -> Self { + Self { proxy } + } +} diff --git a/mpris2/src/playlists/id.rs b/mpris2/src/playlists/id.rs new file mode 100644 index 0000000..fb45cae --- /dev/null +++ b/mpris2/src/playlists/id.rs @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: MPL-2.0 +use serde::{Deserialize, Serialize}; +use std::{ + cmp::{Ord, Ordering, PartialOrd}, + fmt::{self, Display}, + ops::Deref, +}; +use zvariant::{ObjectPath, OwnedObjectPath, Type, Value}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Type, Serialize, Deserialize, Value)] +pub struct PlaylistId(OwnedObjectPath); + +impl PlaylistId { + pub fn into_inner(self) -> OwnedObjectPath { + self.0 + } + + pub fn into_static_path(self) -> ObjectPath<'static> { + self.0.into_inner().into_owned() + } +} + +impl Deref for PlaylistId { + type Target = OwnedObjectPath; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> AsRef> for PlaylistId { + fn as_ref(&self) -> &ObjectPath<'a> { + &self.0 + } +} + +impl PartialOrd for PlaylistId { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.as_str().partial_cmp(other.0.as_str()) + } +} + +impl Ord for PlaylistId { + fn cmp(&self, other: &Self) -> Ordering { + self.0.as_str().cmp(other.0.as_str()) + } +} + +impl Display for PlaylistId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} diff --git a/mpris2/src/playlists/ordering.rs b/mpris2/src/playlists/ordering.rs new file mode 100644 index 0000000..444dd23 --- /dev/null +++ b/mpris2/src/playlists/ordering.rs @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: MPL-2.0 +use crate::error::{Error, Result}; +use serde::{ + de::{self, Deserialize, Visitor}, + ser::{Serialize, Serializer}, +}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; +use zvariant::{OwnedValue, Signature, Type, Value}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub enum PlaylistOrdering { + /// Alphabetical ordering by name, ascending. + Alphabetical, + /// Ordering by creation date, oldest first. + CreationDate, + /// Ordering by last modified date, oldest first. + ModifiedDate, + /// Ordering by date of last playback, oldest first. + LastPlayDate, + /// A user-defined ordering. + UserDefined, +} + +impl Type for PlaylistOrdering { + fn signature() -> Signature<'static> { + String::signature() + } +} + +impl<'a> TryFrom> for PlaylistOrdering { + type Error = Error; + + fn try_from(value: Value<'a>) -> Result { + match value { + Value::Str(value) => Self::from_str(&value), + _ => Err(Error::IncorrectValue { + wanted: "Str", + actual: OwnedValue::from(value), + }), + } + } +} + +impl<'a> From for Value<'a> { + fn from(ordering: PlaylistOrdering) -> Self { + Value::Str(ordering.to_string().into()) + } +} + +impl FromStr for PlaylistOrdering { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().trim() { + "alphabetical" => Ok(Self::Alphabetical), + "created" => Ok(Self::CreationDate), + "modified" => Ok(Self::ModifiedDate), + "played" => Ok(Self::LastPlayDate), + "user" => Ok(Self::UserDefined), + _ => Err(Error::InvalidEnum { + got: s.to_string(), + expected: &["Alphabetical", "Created", "Modified", "Played", "User"], + }), + } + } +} + +impl Display for PlaylistOrdering { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Self::Alphabetical => "Alphabetical", + Self::CreationDate => "Created", + Self::ModifiedDate => "Modified", + Self::LastPlayDate => "Played", + Self::UserDefined => "User", + } + ) + } +} + +impl Serialize for PlaylistOrdering { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_str()) + } +} + +impl<'de> Deserialize<'de> for PlaylistOrdering { + fn deserialize(deserializer: D) -> std::result::Result + where + D: de::Deserializer<'de>, + { + deserializer.deserialize_str(PlaylistOrderingVisitor) + } +} + +struct PlaylistOrderingVisitor; + +impl Visitor<'_> for PlaylistOrderingVisitor { + type Value = PlaylistOrdering; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a string") + } + + fn visit_str(self, s: &str) -> std::result::Result + where + E: de::Error, + { + PlaylistOrdering::from_str(s).map_err(de::Error::custom) + } +} diff --git a/mpris2/src/playlists/playlist.rs b/mpris2/src/playlists/playlist.rs new file mode 100644 index 0000000..59114a1 --- /dev/null +++ b/mpris2/src/playlists/playlist.rs @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: MPL-2.0 +use super::id::PlaylistId; +use serde::{Deserialize, Serialize}; +use zbus::zvariant::{Type, Value}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Type, Value, Serialize, Deserialize)] +pub struct Playlist((PlaylistId, String, String)); + +impl Playlist { + pub fn id(&self) -> &PlaylistId { + &self.0 .0 + } + + pub fn name(&self) -> &str { + &self.0 .1 + } + + pub fn icon(&self) -> &str { + &self.0 .2 + } +} diff --git a/mpris2/src/track.rs b/mpris2/src/track.rs new file mode 100644 index 0000000..b107510 --- /dev/null +++ b/mpris2/src/track.rs @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: MPL-2.0 +use serde::{Deserialize, Serialize}; +use std::{ + cmp::Ordering, + fmt::{self, Display}, + ops::Deref, +}; +use zbus::zvariant::{ObjectPath, OwnedObjectPath, Type, Value}; + +/// A reference to an MPRIS track. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Type, Serialize, Deserialize, Value)] +pub struct TrackId(OwnedObjectPath); + +impl TrackId { + pub fn into_inner(self) -> OwnedObjectPath { + self.0 + } + + pub fn into_static_path(self) -> ObjectPath<'static> { + self.0.into_inner().into_owned() + } +} + +impl Deref for TrackId { + type Target = OwnedObjectPath; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'a> AsRef> for TrackId { + fn as_ref(&self) -> &ObjectPath<'a> { + &self.0 + } +} + +impl PartialOrd for TrackId { + fn partial_cmp(&self, other: &Self) -> Option { + self.0.as_str().partial_cmp(other.0.as_str()) + } +} + +impl Ord for TrackId { + fn cmp(&self, other: &Self) -> Ordering { + self.0.as_str().cmp(other.0.as_str()) + } +} + +impl Display for TrackId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.as_str()) + } +} diff --git a/mpris2/src/track_list.rs b/mpris2/src/track_list.rs new file mode 100644 index 0000000..9da82da --- /dev/null +++ b/mpris2/src/track_list.rs @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: MPL-2.0 +use crate::{ + bindings::track_list::TrackListProxy, + error::{Error, Result}, + metadata::Metadata, + track::TrackId, +}; +use std::{collections::BTreeMap, ops::Deref}; +use zbus::{names::OwnedBusName, Connection}; + +#[derive(Debug, Clone)] +pub struct TrackList { + proxy: TrackListProxy<'static>, +} + +impl TrackList { + /// Creates a new instance of the `org.mpris.MediaPlayer2.TrackList` interface. + pub async fn new(connection: &Connection, name: OwnedBusName) -> Result { + TrackListProxy::builder(connection) + .destination(name)? + .build() + .await + .map(Self::from) + .map_err(Error::from) + } + + /// Adds a new track to this track list. + pub async fn add_track( + &self, + uri: S, + after: &TrackId, + set_as_current: bool, + ) -> Result<()> { + let uri = uri.to_string(); + self.proxy + .add_track(&uri, after, set_as_current) + .await + .map_err(Error::from) + } + + /// Gets the metadata of the given tracks. + pub async fn get_tracks_metadata>( + &self, + tracks: T, + ) -> Result> { + self.proxy + .get_tracks_metadata(tracks.as_ref().to_vec()) + .await + .map(|x| x.into_iter().map(Metadata::from).collect()) + .map_err(Error::from) + } + + /// Goes to the specified track. + pub async fn go_to(&self, track: &TrackId) -> Result<()> { + self.proxy.go_to(track).await.map_err(Error::from) + } + + /// Removes the specified track. + pub async fn remove(&self, track: &TrackId) -> Result<()> { + self.proxy.remove_track(track).await.map_err(Error::from) + } + + /// Returns a list of all available [Track]s. + pub async fn tracks(&self) -> Result> { + self.proxy + .tracks() + .await + .map(|x| x.into_iter().map(TrackId::from).collect()) + .map_err(Error::from) + } + + /// Returns a list of all available [Track]s and their associated metadata, + /// in order. + pub async fn detailed_tracks(&self) -> Result> { + let tracks = self.tracks().await?; + let metadata = self.get_tracks_metadata(&tracks).await?; + Ok(tracks.into_iter().zip(metadata.into_iter()).collect()) + } +} + +impl Deref for TrackList { + type Target = TrackListProxy<'static>; + + fn deref(&self) -> &Self::Target { + &self.proxy + } +} + +impl From> for TrackList { + fn from(proxy: TrackListProxy<'static>) -> Self { + Self { proxy } + } +} diff --git a/timedate/.gitignore b/timedate/.gitignore new file mode 100644 index 0000000..f2f9e58 --- /dev/null +++ b/timedate/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock \ No newline at end of file diff --git a/timedate/Cargo.toml b/timedate/Cargo.toml new file mode 100644 index 0000000..cb34cb6 --- /dev/null +++ b/timedate/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "timedate-zbus" +version = "0.1.0" +description = "a dbus client (using zbus) for timedate" +repository = "https://github.com/pop-os/dbus-settings-bindings" +license = "MPL-2.0" +edition = "2021" +categories = ["os::unix-apis"] +keywords = ["systemd", "timedate", "zbus"] + +[package.metadata.docs.rs] +all-features = true + +[dependencies] +zbus = "3.8.0" + +[dev-dependencies] +chrono = "0.4.23" +chrono-tz = "0.8.1" + +[dev-dependencies.zbus] +version = "3.8.0" +default-features = false +features = ["tokio"] + +[dev-dependencies.tokio] +version = "1.25.0" +features = ["full"] diff --git a/timedate/LICENSE b/timedate/LICENSE new file mode 100644 index 0000000..fa0086a --- /dev/null +++ b/timedate/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/timedate/README.md b/timedate/README.md new file mode 100644 index 0000000..95f8210 --- /dev/null +++ b/timedate/README.md @@ -0,0 +1,3 @@ +# timedate-zbus + +A zbus client proxy for [org.freedesktop.timedate1](https://www.freedesktop.org/software/systemd/man/org.freedesktop.timedate1.html). The [timedatectl example](./examples/timedatectl.rs) outputs in the same format as the `timedatectl` command. diff --git a/timedate/examples/timedatectl.rs b/timedate/examples/timedatectl.rs new file mode 100644 index 0000000..d1e1239 --- /dev/null +++ b/timedate/examples/timedatectl.rs @@ -0,0 +1,56 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use chrono::TimeZone; + +const TZ_FORMAT: &str = "%a %Y-%m-%d %H:%M:%S %Z"; +const RTC_FORMAT: &str = "%a %Y-%m-%d %H:%M:%S"; +const CHOICES: &[&str] = &["no", "yes"]; + +#[tokio::main] +pub async fn main() -> zbus::Result<()> { + let connection = zbus::Connection::system().await?; + let proxy = timedate_zbus::TimeDateProxy::new(&connection).await?; + + let ntp_service = if proxy.ntp().await? { + "active" + } else { + "inactive" + }; + + let rtc_in_local = proxy.local_rtc().await?; + let rtc_time_usecs = proxy.rtctime_usec().await?; + let time_usecs = proxy.time_usec().await?; + let timezone = proxy.timezone().await?; + + let tz: chrono_tz::Tz = timezone.parse().unwrap(); + + let datetime = tz.timestamp_millis_opt((time_usecs / 1000) as i64).unwrap(); + + let rtc_millis = (rtc_time_usecs / 1000) as i64; + let rtc_time = (if rtc_in_local { + tz.timestamp_millis_opt(rtc_millis).unwrap() + } else { + chrono_tz::UTC.timestamp_millis_opt(rtc_millis).unwrap() + }) + .format(RTC_FORMAT); + + let local = datetime.format(TZ_FORMAT); + let universal = datetime.with_timezone(&chrono_tz::UTC).format(TZ_FORMAT); + let tz_string = datetime.format("%Z, %z"); + + let rtc_in_local = CHOICES[usize::from(rtc_in_local)]; + let synchronized = CHOICES[usize::from(proxy.ntp_synchronized().await.unwrap_or_default())]; + + println!( + " Local time: {local} + Universal time: {universal} + RTC time: {rtc_time} + Time zone: {timezone} ({tz_string}) +System clock synchronized: {synchronized} + NTP Service: {ntp_service} + RTC in local TZ: {rtc_in_local}" + ); + + Ok(()) +} diff --git a/timedate/src/lib.rs b/timedate/src/lib.rs new file mode 100644 index 0000000..c6ed99d --- /dev/null +++ b/timedate/src/lib.rs @@ -0,0 +1,75 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! # DBus interface proxy for: `org.freedesktop.timedate1` +//! +//! This code was generated by `zbus-xmlgen` `3.1.0` from DBus introspection data. +//! Source: `Interface '/org/freedesktop/timedate1' from service 'org.freedesktop.timedate1' on system bus`. +//! +//! This DBus object implements +//! [standard DBus interfaces](https://dbus.freedesktop.org/doc/dbus-specification.html), +//! (`org.freedesktop.DBus.*`) for which the following zbus proxies can be used: +//! +//! * [`zbus::fdo::PeerProxy`] +//! * [`zbus::fdo::IntrospectableProxy`] +//! * [`zbus::fdo::PropertiesProxy`] +//! + +use zbus::dbus_proxy; + +#[dbus_proxy( + interface = "org.freedesktop.timedate1", + default_service = "org.freedesktop.timedate1", + default_path = "/org/freedesktop/timedate1" +)] +trait TimeDate { + /// A list of time zones known on the local system as an array of names. + fn list_timezones(&self) -> zbus::Result>; + + /// Control whether the RTC is in local time zone or UTC. + #[dbus_proxy(name = "SetLocalRTC")] + fn set_local_rtc( + &self, + local_rtc: bool, + fix_system: bool, + interactive: bool, + ) -> zbus::Result<()>; + + /// Control whether the system clock is synchronized with the network using `systemd-timesyncd`. + #[dbus_proxy(name = "SetNTP")] + fn set_ntp(&self, use_ntp: bool, interactive: bool) -> zbus::Result<()>; + + /// Change the system clock. + fn set_time(&self, usec_utc: i64, relative: bool, interactive: bool) -> zbus::Result<()>; + + /// Set the system time zone. + fn set_timezone(&self, timezone: &str, interactive: bool) -> zbus::Result<()>; + + /// Shows whether a service to perform time synchronization over network is available. + #[dbus_proxy(property, name = "CanNTP")] + fn can_ntp(&self) -> zbus::Result; + + /// Shows whether the RTC is configured to use UTC or the local time zone. + #[dbus_proxy(property, name = "LocalRTC")] + fn local_rtc(&self) -> zbus::Result; + + /// Shows whether the NTP service is enabled. + #[dbus_proxy(property, name = "NTP")] + fn ntp(&self) -> zbus::Result; + + /// Shows whether the kernel reports the time as synchronized. + #[dbus_proxy(property, name = "NTPSynchronized")] + fn ntp_synchronized(&self) -> zbus::Result; + + /// Shows the current time in RTC. + #[dbus_proxy(property, name = "RTCTimeUSec")] + fn rtctime_usec(&self) -> zbus::Result; + + /// Shows the current time. + #[dbus_proxy(property, name = "TimeUSec")] + fn time_usec(&self) -> zbus::Result; + + /// Shows the currently-configured time zone. + #[dbus_proxy(property)] + fn timezone(&self) -> zbus::Result; +} diff --git a/upower/.gitignore b/upower/.gitignore new file mode 100644 index 0000000..6936990 --- /dev/null +++ b/upower/.gitignore @@ -0,0 +1,3 @@ +/target +**/*.rs.bk +Cargo.lock diff --git a/upower/Cargo.toml b/upower/Cargo.toml new file mode 100644 index 0000000..c63febd --- /dev/null +++ b/upower/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "upower_dbus" +version = "0.3.2" +authors = ["Michael Murphy "] +repository = "https://github.com/pop-os/dbus-settings-bindings" +description = "a dbus client (using zbus) for upower" +readme = "README.md" +license = "MIT" +categories = ["os::unix-apis"] +keywords = ["linux", "systemd", "logind", "dbus", "zbus"] +edition = "2018" + +[dependencies] +serde = "1.0.152" +serde_repr = "0.1.10" +zbus = "3.8.0" + +[dev-dependencies] +futures = "0.3.26" diff --git a/upower/LICENSE b/upower/LICENSE new file mode 100644 index 0000000..fa0086a --- /dev/null +++ b/upower/LICENSE @@ -0,0 +1,373 @@ +Mozilla Public License Version 2.0 +================================== + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. \ No newline at end of file diff --git a/upower/README.md b/upower/README.md new file mode 100644 index 0000000..a71dc1a --- /dev/null +++ b/upower/README.md @@ -0,0 +1,56 @@ +# upower-dbus + +A Rust library which interfaces with UPower status information through dbus. + +## Examples + +### Detecting if the system is running on battery + +```rust,no_run +extern crate upower_dbus; + +use futures::stream::StreamExt; +use upower_dbus::UPowerProxy; + +fn main() -> zbus::Result<()> { + futures::executor::block_on(async move { + let connection = zbus::Connection::system().await?; + + let upower = UPowerProxy::new(&connection).await?; + + println!("On Battery: {:?}", upower.on_battery().await); + + let mut stream = upower.receive_on_battery_changed().await; + + while let Some(event) = stream.next().await { + println!("On Battery: {:?}", event.get().await); + } + + Ok(()) + }) +} + +``` + +### Getting the current battery status as a percentage + +```rust,no_run +extern crate upower_dbus; + +use upower_dbus::UPowerProxy; + +fn main() -> zbus::Result<()> { + futures::executor::block_on(async move { + let connection = zbus::Connection::system().await?; + + let upower = UPowerProxy::new(&connection).await?; + + let device = upower.get_display_device().await?; + + println!("Battery: {:?}", device.percentage().await); + + Ok(()) + }) +} + +``` diff --git a/upower/examples/device.rs b/upower/examples/device.rs new file mode 100644 index 0000000..2ade4bd --- /dev/null +++ b/upower/examples/device.rs @@ -0,0 +1,26 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +extern crate upower_dbus; + +use upower_dbus::UPowerProxy; + +fn main() -> zbus::Result<()> { + futures::executor::block_on(async move { + let connection = zbus::Connection::system().await?; + + let upower = UPowerProxy::new(&connection).await?; + + let device = upower.get_display_device().await?; + + println!("BatteryLevel: {:?}", device.battery_level().await); + println!("IconName: {:?}", device.icon_name().await); + println!("IsPresent: {:?}", device.is_present().await); + println!("Online: {:?}", device.online().await); + println!("Percentage: {:?}", device.percentage().await); + println!("State: {:?}", device.state().await); + println!("Type: {:?}", device.type_().await); + + Ok(()) + }) +} diff --git a/upower/examples/on_battery.rs b/upower/examples/on_battery.rs new file mode 100644 index 0000000..e920d0e --- /dev/null +++ b/upower/examples/on_battery.rs @@ -0,0 +1,25 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +extern crate upower_dbus; + +use futures::stream::StreamExt; +use upower_dbus::UPowerProxy; + +fn main() -> zbus::Result<()> { + futures::executor::block_on(async move { + let connection = zbus::Connection::system().await?; + + let upower = UPowerProxy::new(&connection).await?; + + println!("On Battery: {:?}", upower.on_battery().await); + + let mut stream = upower.receive_on_battery_changed().await; + + while let Some(event) = stream.next().await { + eprintln!("{:?}", event.get().await); + } + + Ok(()) + }) +} diff --git a/upower/src/device.rs b/upower/src/device.rs new file mode 100644 index 0000000..7423ad1 --- /dev/null +++ b/upower/src/device.rs @@ -0,0 +1,122 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use serde_repr::{Deserialize_repr, Serialize_repr}; +use zbus::dbus_proxy; +use zbus::zvariant::OwnedValue; + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize_repr, Serialize_repr, OwnedValue)] +#[repr(u32)] +pub enum BatteryState { + Unknown = 0, + Charging = 1, + Discharging = 2, + Empty = 3, + FullyCharged = 4, + PendingCharge = 5, + PendingDischarge = 6, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize_repr, Serialize_repr, OwnedValue)] +#[repr(u32)] +pub enum BatteryType { + Unknown = 0, + LinePower = 1, + Battery = 2, + Ups = 3, + Monitor = 4, + Mouse = 5, + Keyboard = 6, + Pda = 7, + Phone = 8, +} + +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize_repr, Serialize_repr, OwnedValue)] +#[repr(u32)] +pub enum BatteryLevel { + Unknown = 0, + None = 1, + Low = 3, + Critical = 4, + Normal = 6, + High = 7, + Full = 8, +} + +#[dbus_proxy( + interface = "org.freedesktop.UPower.Device", + default_service = "org.freedesktop.UPower", + assume_defaults = false +)] +trait Device { + #[dbus_proxy(property)] + fn battery_level(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn capacity(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn energy(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn energy_empty(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn energy_full(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn energy_full_design(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn has_history(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn has_statistics(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn icon_name(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn is_present(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn is_rechargeable(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn luminosity(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn model(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn native_path(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn online(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn percentage(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn power_supply(&self) -> zbus::Result; + + fn refresh(&self) -> zbus::Result<()>; + + #[dbus_proxy(property)] + fn serial(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn state(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn temperature(&self) -> zbus::Result; + + #[dbus_proxy(property, name = "Type")] + fn type_(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn vendor(&self) -> zbus::Result; + + #[dbus_proxy(property)] + fn voltage(&self) -> zbus::Result; +} diff --git a/upower/src/lib.rs b/upower/src/lib.rs new file mode 100644 index 0000000..a6cc00b --- /dev/null +++ b/upower/src/lib.rs @@ -0,0 +1,9 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 +#![doc = include_str!("../README.md")] + +mod device; +mod upower; + +pub use self::device::*; +pub use self::upower::*; diff --git a/upower/src/upower.rs b/upower/src/upower.rs new file mode 100644 index 0000000..ed7ccf5 --- /dev/null +++ b/upower/src/upower.rs @@ -0,0 +1,43 @@ +// Copyright 2021 System76 +// SPDX-License-Identifier: MPL-2.0 + +use zbus::dbus_proxy; + +use crate::device::{DeviceProxy, DeviceProxyBlocking}; + +#[dbus_proxy(interface = "org.freedesktop.UPower", assume_defaults = true)] +trait UPower { + /// EnumerateDevices method + fn enumerate_devices(&self) -> zbus::Result>; + + /// GetCriticalAction method + fn get_critical_action(&self) -> zbus::Result; + + /// GetDisplayDevice method + #[dbus_proxy(object = "Device")] + fn get_display_device(&self); + + /// DeviceAdded signal + #[dbus_proxy(signal)] + fn device_added(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// DeviceRemoved signal + #[dbus_proxy(signal)] + fn device_removed(&self, device: zbus::zvariant::ObjectPath<'_>) -> zbus::Result<()>; + + /// DaemonVersion property + #[dbus_proxy(property)] + fn daemon_version(&self) -> zbus::Result; + + /// LidIsClosed property + #[dbus_proxy(property)] + fn lid_is_closed(&self) -> zbus::Result; + + /// LidIsPresent property + #[dbus_proxy(property)] + fn lid_is_present(&self) -> zbus::Result; + + /// OnBattery property + #[dbus_proxy(property)] + fn on_battery(&self) -> zbus::Result; +}