mirror of
https://github.com/jkcoxson/idevice.git
synced 2026-03-02 06:26:15 +01:00
feat(springboard): add get_icon_state and set_icon_state methods (#63)
* feat(springboard): add get_icon_state method for reading home screen layout Add get_icon_state() method to SpringBoardServicesClient that retrieves the current home screen icon layout from iOS devices. Features: - Read complete home screen layout including icon positions and folders - Support for optional formatVersion parameter - Works on all iOS versions (tested on iOS 18.7.3) - Comprehensive documentation with usage examples Note: This PR intentionally does NOT include set_icon_state() as that functionality is non-operational on iOS 18+ (see issue #62 for details). Tested on: - Device: iPhone 16,2 (iPhone 15 Pro) - iOS: 18.7.3 (Build 22H217) * feat(springboard): add set_icon_state method with date precision fix - Implement set_icon_state() to modify home screen layout - Implement set_icon_state_with_version() with format_version parameter - Add truncate_dates_to_seconds() to convert nanosecond precision dates to second precision - Fix iOS compatibility issue where high-precision dates were rejected - Successfully tested on iOS 18.7.3 (previously believed to be restricted) - Follows pymobiledevice3 implementation pattern * refactor(utils): extract truncate_dates_to_seconds to utils::plist module - Move date truncation logic from springboardservices to reusable utils::plist module - Add comprehensive unit tests for date truncation functionality - Add public API documentation for the utility function - This makes the date normalization logic available for other services that may need it * perf(springboard): normalize dates on read instead of write - Move date truncation from set_icon_state to get_icon_state - Eliminates unnecessary clone() operation in set_icon_state - Better performance when setting icon state multiple times - Cleaner API: data from get_icon_state is directly usable in set_icon_state - Users don't need to worry about date precision issues * refactor(springboard): address PR feedback - use Option<&str> and add error validation - Change format_version parameter from Option<String> to Option<&str> for consistency - Remove outdated iOS 18+ restriction comments since setIconState works on iOS 18+ - Add error validation to get_icon_state method similar to get_icon_pngdata - Update documentation to reflect accurate iOS compatibility * Fix cargo clippy warnings * Fix clippy warnings in plist.rs * Add springboard CLI commands --------- Co-authored-by: Jackson Coxson <jkcoxson@gmail.com>
This commit is contained in:
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1782,9 +1782,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "plist-macro"
|
name = "plist-macro"
|
||||||
version = "0.1.0"
|
version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cb72007326fe20721ef27304fcf2d1bd5877b92d13dbd8df735fd33407e31c2a"
|
checksum = "8888e02e251eba3258cc58fb79f0d8675c34b3428749e738562d58a0271bf035"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"plist",
|
"plist",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ tokio-openssl = { version = "0.6", optional = true }
|
|||||||
openssl = { version = "0.10", optional = true }
|
openssl = { version = "0.10", optional = true }
|
||||||
|
|
||||||
plist = { version = "1.8" }
|
plist = { version = "1.8" }
|
||||||
plist-macro = { version = "0.1" }
|
plist-macro = { version = "0.1.3" }
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
ns-keyed-archive = { version = "0.1.4", optional = true }
|
ns-keyed-archive = { version = "0.1.4", optional = true }
|
||||||
crossfire = { version = "2.1", optional = true }
|
crossfire = { version = "2.1", optional = true }
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
|
//! Provides functionality for interacting with the SpringBoard services on iOS devices,
|
||||||
//! which manages home screen and app icon related operations.
|
//! which manages home screen and app icon related operations.
|
||||||
|
|
||||||
use crate::{Idevice, IdeviceError, IdeviceService, obf};
|
use crate::{Idevice, IdeviceError, IdeviceService, obf, utils::plist::truncate_dates_to_seconds};
|
||||||
|
|
||||||
/// Client for interacting with the iOS SpringBoard services
|
/// Client for interacting with the iOS SpringBoard services
|
||||||
///
|
///
|
||||||
@@ -70,4 +70,132 @@ impl SpringBoardServicesClient {
|
|||||||
_ => Err(IdeviceError::UnexpectedResponse),
|
_ => Err(IdeviceError::UnexpectedResponse),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Retrieves the current icon state from the device
|
||||||
|
///
|
||||||
|
/// The icon state contains the layout and organization of all apps on the home screen,
|
||||||
|
/// including folder structures and icon positions. This is a read-only operation.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `format_version` - Optional format version string for the icon state format
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// A plist Value containing the complete icon state structure
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The response is malformed
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use idevice::services::springboardservices::SpringBoardServicesClient;
|
||||||
|
///
|
||||||
|
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
|
||||||
|
/// let icon_state = client.get_icon_state(None).await?;
|
||||||
|
/// println!("Icon state: {:?}", icon_state);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
/// This method successfully reads the home screen layout on all iOS versions.
|
||||||
|
pub async fn get_icon_state(
|
||||||
|
&mut self,
|
||||||
|
format_version: Option<&str>,
|
||||||
|
) -> Result<plist::Value, IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "getIconState",
|
||||||
|
"formatVersion":? format_version,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
let mut res = self.idevice.read_plist_value().await?;
|
||||||
|
|
||||||
|
// Some devices may return an error dictionary instead of icon state.
|
||||||
|
// Detect this and surface it as an UnexpectedResponse, similar to get_icon_pngdata.
|
||||||
|
if let plist::Value::Dictionary(ref dict) = res
|
||||||
|
&& (dict.contains_key("error") || dict.contains_key("Error"))
|
||||||
|
{
|
||||||
|
return Err(IdeviceError::UnexpectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut res);
|
||||||
|
|
||||||
|
Ok(res)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the icon state on the device
|
||||||
|
///
|
||||||
|
/// This method allows you to modify the home screen layout by providing a new icon state.
|
||||||
|
/// The icon state structure should match the format returned by `get_icon_state`.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `icon_state` - A plist Value containing the complete icon state structure
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Ok(()) if the icon state was successfully set
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The icon state format is invalid
|
||||||
|
/// - The device rejects the new layout
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust
|
||||||
|
/// use idevice::services::springboardservices::SpringBoardServicesClient;
|
||||||
|
///
|
||||||
|
/// let mut client = SpringBoardServicesClient::connect(&provider).await?;
|
||||||
|
/// let mut icon_state = client.get_icon_state(None).await?;
|
||||||
|
///
|
||||||
|
/// // Modify the icon state (e.g., swap two icons)
|
||||||
|
/// // ... modify icon_state ...
|
||||||
|
///
|
||||||
|
/// client.set_icon_state(icon_state).await?;
|
||||||
|
/// println!("Icon state updated successfully");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Notes
|
||||||
|
/// - Changes take effect immediately
|
||||||
|
/// - The device may validate the icon state structure before applying
|
||||||
|
/// - Invalid icon states will be rejected by the device
|
||||||
|
pub async fn set_icon_state(&mut self, icon_state: plist::Value) -> Result<(), IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "setIconState",
|
||||||
|
"iconState": icon_state,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the icon state with a specific format version
|
||||||
|
///
|
||||||
|
/// This is similar to `set_icon_state` but allows specifying a format version.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `icon_state` - A plist Value containing the complete icon state structure
|
||||||
|
/// * `format_version` - Optional format version string
|
||||||
|
///
|
||||||
|
/// # Returns
|
||||||
|
/// Ok(()) if the icon state was successfully set
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns `IdeviceError` if:
|
||||||
|
/// - Communication fails
|
||||||
|
/// - The icon state format is invalid
|
||||||
|
/// - The device rejects the new layout
|
||||||
|
pub async fn set_icon_state_with_version(
|
||||||
|
&mut self,
|
||||||
|
icon_state: plist::Value,
|
||||||
|
format_version: Option<&str>,
|
||||||
|
) -> Result<(), IdeviceError> {
|
||||||
|
let req = crate::plist!({
|
||||||
|
"command": "setIconState",
|
||||||
|
"iconState": icon_state,
|
||||||
|
"formatVersion":? format_version,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.idevice.send_plist(req).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,5 @@
|
|||||||
|
|
||||||
#[cfg(all(feature = "afc", feature = "installation_proxy"))]
|
#[cfg(all(feature = "afc", feature = "installation_proxy"))]
|
||||||
pub mod installation;
|
pub mod installation;
|
||||||
|
|
||||||
|
pub mod plist;
|
||||||
|
|||||||
155
idevice/src/utils/plist.rs
Normal file
155
idevice/src/utils/plist.rs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
/// Utilities for working with plist values
|
||||||
|
///
|
||||||
|
/// Truncates all Date values in a plist structure to second precision.
|
||||||
|
///
|
||||||
|
/// This function recursively walks through a plist Value and truncates any Date values
|
||||||
|
/// from nanosecond precision to second precision. This is necessary for compatibility
|
||||||
|
/// with iOS devices that reject high-precision date formats.
|
||||||
|
///
|
||||||
|
/// # Arguments
|
||||||
|
/// * `value` - The plist Value to normalize (modified in place)
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
/// ```rust,no_run
|
||||||
|
/// use idevice::utils::plist::truncate_dates_to_seconds;
|
||||||
|
/// use plist::Value;
|
||||||
|
///
|
||||||
|
/// let mut icon_state = Value::Array(vec![]);
|
||||||
|
/// truncate_dates_to_seconds(&mut icon_state);
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Details
|
||||||
|
/// - Converts dates from format: `2026-01-17T03:09:58.332738876Z` (nanosecond precision)
|
||||||
|
/// - To format: `2026-01-17T03:09:58Z` (second precision)
|
||||||
|
/// - Recursively processes Arrays and Dictionaries
|
||||||
|
/// - Other value types are left unchanged
|
||||||
|
pub fn truncate_dates_to_seconds(value: &mut plist::Value) {
|
||||||
|
match value {
|
||||||
|
plist::Value::Date(date) => {
|
||||||
|
let xml_string = date.to_xml_format();
|
||||||
|
if let Some(dot_pos) = xml_string.find('.')
|
||||||
|
&& xml_string[dot_pos..].contains('Z')
|
||||||
|
{
|
||||||
|
let truncated_string = format!("{}Z", &xml_string[..dot_pos]);
|
||||||
|
if let Ok(new_date) = plist::Date::from_xml_format(&truncated_string) {
|
||||||
|
*date = new_date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plist::Value::Array(arr) => {
|
||||||
|
for item in arr.iter_mut() {
|
||||||
|
truncate_dates_to_seconds(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
plist::Value::Dictionary(dict) => {
|
||||||
|
for (_, v) in dict.iter_mut() {
|
||||||
|
truncate_dates_to_seconds(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_date_with_nanoseconds() {
|
||||||
|
let date_str = "2026-01-17T03:09:58.332738876Z";
|
||||||
|
let date = plist::Date::from_xml_format(date_str).unwrap();
|
||||||
|
let mut value = plist::Value::Date(date);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Date(truncated_date) = value {
|
||||||
|
let result = truncated_date.to_xml_format();
|
||||||
|
assert!(
|
||||||
|
!result.contains('.'),
|
||||||
|
"Date should not contain fractional seconds"
|
||||||
|
);
|
||||||
|
assert!(result.ends_with('Z'), "Date should end with Z");
|
||||||
|
assert!(
|
||||||
|
result.starts_with("2026-01-17T03:09:58"),
|
||||||
|
"Date should preserve main timestamp"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
panic!("Value should still be a Date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_date_already_truncated() {
|
||||||
|
let date_str = "2026-01-17T03:09:58Z";
|
||||||
|
let date = plist::Date::from_xml_format(date_str).unwrap();
|
||||||
|
let original_format = date.to_xml_format();
|
||||||
|
let mut value = plist::Value::Date(date);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Date(truncated_date) = value {
|
||||||
|
let result = truncated_date.to_xml_format();
|
||||||
|
assert_eq!(
|
||||||
|
result, original_format,
|
||||||
|
"Already truncated date should remain unchanged"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_dates_in_array() {
|
||||||
|
let date1 = plist::Date::from_xml_format("2026-01-17T03:09:58.123456Z").unwrap();
|
||||||
|
let date2 = plist::Date::from_xml_format("2026-01-18T04:10:59.987654Z").unwrap();
|
||||||
|
let mut value =
|
||||||
|
plist::Value::Array(vec![plist::Value::Date(date1), plist::Value::Date(date2)]);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Array(arr) = value {
|
||||||
|
for item in arr {
|
||||||
|
if let plist::Value::Date(date) = item {
|
||||||
|
let formatted = date.to_xml_format();
|
||||||
|
assert!(
|
||||||
|
!formatted.contains('.'),
|
||||||
|
"Dates in array should be truncated"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_truncate_dates_in_dictionary() {
|
||||||
|
let date = plist::Date::from_xml_format("2026-01-17T03:09:58.999999Z").unwrap();
|
||||||
|
let mut dict = plist::Dictionary::new();
|
||||||
|
dict.insert("timestamp".to_string(), plist::Value::Date(date));
|
||||||
|
let mut value = plist::Value::Dictionary(dict);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut value);
|
||||||
|
|
||||||
|
if let plist::Value::Dictionary(dict) = value
|
||||||
|
&& let Some(plist::Value::Date(date)) = dict.get("timestamp")
|
||||||
|
{
|
||||||
|
let formatted = date.to_xml_format();
|
||||||
|
assert!(
|
||||||
|
!formatted.contains('.'),
|
||||||
|
"Date in dictionary should be truncated"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_other_value_types_unchanged() {
|
||||||
|
let mut string_val = plist::Value::String("test".to_string());
|
||||||
|
let mut int_val = plist::Value::Integer(42.into());
|
||||||
|
let mut bool_val = plist::Value::Boolean(true);
|
||||||
|
|
||||||
|
truncate_dates_to_seconds(&mut string_val);
|
||||||
|
truncate_dates_to_seconds(&mut int_val);
|
||||||
|
truncate_dates_to_seconds(&mut bool_val);
|
||||||
|
|
||||||
|
assert!(matches!(string_val, plist::Value::String(_)));
|
||||||
|
assert!(matches!(int_val, plist::Value::Integer(_)));
|
||||||
|
assert!(matches!(bool_val, plist::Value::Boolean(_)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ ureq = { version = "3" }
|
|||||||
clap = { version = "4.5" }
|
clap = { version = "4.5" }
|
||||||
jkcli = { version = "0.1" }
|
jkcli = { version = "0.1" }
|
||||||
plist = { version = "1.7" }
|
plist = { version = "1.7" }
|
||||||
plist-macro = { version = "0.1" }
|
plist-macro = { version = "0.1.3" }
|
||||||
ns-keyed-archive = "0.1.2"
|
ns-keyed-archive = "0.1.2"
|
||||||
uuid = "1.16"
|
uuid = "1.16"
|
||||||
futures-util = { version = "0.3" }
|
futures-util = { version = "0.3" }
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ mod process_control;
|
|||||||
mod remotexpc;
|
mod remotexpc;
|
||||||
mod restore_service;
|
mod restore_service;
|
||||||
mod screenshot;
|
mod screenshot;
|
||||||
|
mod springboardservices;
|
||||||
mod syslog_relay;
|
mod syslog_relay;
|
||||||
|
|
||||||
mod pcap;
|
mod pcap;
|
||||||
@@ -120,6 +121,7 @@ async fn main() {
|
|||||||
.with_subcommand("remotexpc", remotexpc::register())
|
.with_subcommand("remotexpc", remotexpc::register())
|
||||||
.with_subcommand("restore_service", restore_service::register())
|
.with_subcommand("restore_service", restore_service::register())
|
||||||
.with_subcommand("screenshot", screenshot::register())
|
.with_subcommand("screenshot", screenshot::register())
|
||||||
|
.with_subcommand("springboard", springboardservices::register())
|
||||||
.with_subcommand("syslog_relay", syslog_relay::register())
|
.with_subcommand("syslog_relay", syslog_relay::register())
|
||||||
.subcommand_required(true)
|
.subcommand_required(true)
|
||||||
.collect()
|
.collect()
|
||||||
@@ -236,6 +238,9 @@ async fn main() {
|
|||||||
"screenshot" => {
|
"screenshot" => {
|
||||||
screenshot::main(sub_args, provider).await;
|
screenshot::main(sub_args, provider).await;
|
||||||
}
|
}
|
||||||
|
"springboard" => {
|
||||||
|
springboardservices::main(sub_args, provider).await;
|
||||||
|
}
|
||||||
"syslog_relay" => {
|
"syslog_relay" => {
|
||||||
syslog_relay::main(sub_args, provider).await;
|
syslog_relay::main(sub_args, provider).await;
|
||||||
}
|
}
|
||||||
|
|||||||
76
tools/src/springboardservices.rs
Normal file
76
tools/src/springboardservices.rs
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
// Jackson Coxson
|
||||||
|
|
||||||
|
use idevice::{
|
||||||
|
IdeviceService, provider::IdeviceProvider, springboardservices::SpringBoardServicesClient,
|
||||||
|
};
|
||||||
|
use jkcli::{CollectedArguments, JkArgument, JkCommand, JkFlag};
|
||||||
|
use plist_macro::{plist_value_to_xml_bytes, pretty_print_plist};
|
||||||
|
|
||||||
|
pub fn register() -> JkCommand {
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Manage the springboard service")
|
||||||
|
.with_subcommand(
|
||||||
|
"get_icon_state",
|
||||||
|
JkCommand::new()
|
||||||
|
.help("Gets the icon state from the device")
|
||||||
|
.with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("Version to query by")
|
||||||
|
.required(false),
|
||||||
|
)
|
||||||
|
.with_flag(
|
||||||
|
JkFlag::new("save")
|
||||||
|
.with_help("Path to save to")
|
||||||
|
.with_argument(JkArgument::new().required(true)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with_subcommand(
|
||||||
|
"set_icon_state",
|
||||||
|
JkCommand::new().help("Sets the icon state").with_argument(
|
||||||
|
JkArgument::new()
|
||||||
|
.with_help("plist to set based on")
|
||||||
|
.required(true),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.subcommand_required(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn main(arguments: &CollectedArguments, provider: Box<dyn IdeviceProvider>) {
|
||||||
|
let mut sbc = SpringBoardServicesClient::connect(&*provider)
|
||||||
|
.await
|
||||||
|
.expect("Failed to connect to springboardservices");
|
||||||
|
|
||||||
|
let (sub_name, sub_args) = arguments.first_subcommand().expect("No subcommand passed");
|
||||||
|
let mut sub_args = sub_args.clone();
|
||||||
|
|
||||||
|
match sub_name.as_str() {
|
||||||
|
"get_icon_state" => {
|
||||||
|
let version: Option<String> = sub_args.next_argument();
|
||||||
|
let version = version.as_deref();
|
||||||
|
let state = sbc
|
||||||
|
.get_icon_state(version)
|
||||||
|
.await
|
||||||
|
.expect("Failed to get icon state");
|
||||||
|
println!("{}", pretty_print_plist(&state));
|
||||||
|
|
||||||
|
if let Some(path) = sub_args.get_flag::<String>("save") {
|
||||||
|
tokio::fs::write(path, plist_value_to_xml_bytes(&state))
|
||||||
|
.await
|
||||||
|
.expect("Failed to save to path");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"set_icon_state" => {
|
||||||
|
let load_path = sub_args.next_argument::<String>().unwrap();
|
||||||
|
let load = tokio::fs::read(load_path)
|
||||||
|
.await
|
||||||
|
.expect("Failed to read plist");
|
||||||
|
let load: plist::Value =
|
||||||
|
plist::from_bytes(&load).expect("Failed to parse bytes as plist");
|
||||||
|
|
||||||
|
sbc.set_icon_state(load)
|
||||||
|
.await
|
||||||
|
.expect("Failed to set icon state");
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user