From d8c7b95e97e45756061bc82ac829ad6e8648003e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 14 Jun 2026 09:27:00 +0000 Subject: [PATCH] ares-install: match reference CLI list behavior Align the `--list` output with webos-tools/cli by hiding non-visible apps, and add the `--listfull`/`-F` and `--type`/`-t` options: - Filter out apps whose `visible` flag is not set, mirroring the reference CLI's non-signage listing behavior. - `--listfull` dumps full app metadata via a port of the reference `convertJsonToList` renderer; remaining fields are captured with a flattened `extra` map so nothing is dropped. - `--type` filters the listed apps by app type. https://claude.ai/code/session_017vAGzSSYNcgdQzJRLWfX47 --- ares-install/src/list.rs | 77 +++++++++++++++++++++++++++++++++++++--- ares-install/src/main.rs | 18 ++++++++-- 2 files changed, 88 insertions(+), 7 deletions(-) diff --git a/ares-install/src/list.rs b/ares-install/src/list.rs index c4beeae..637da54 100644 --- a/ares-install/src/list.rs +++ b/ares-install/src/list.rs @@ -1,9 +1,12 @@ +use std::fmt::Write; + use ares_connection_lib::luna::{Luna, LunaEmptyPayload}; use libssh_rs::Session; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; +use serde_json::{Map, Value}; pub(crate) trait ListApps { - fn list_apps(&self); + fn list_apps(&self, list_full: bool, type_filter: Option<&str>); } #[derive(Deserialize, Debug)] @@ -13,18 +16,24 @@ pub struct ListAppsResponse { pub return_value: bool, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct App { pub id: String, pub version: String, pub r#type: String, pub title: String, + #[serde(skip_serializing_if = "Option::is_none")] pub vendor: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub visible: Option, + // Keep every remaining field so `--listfull` can render the full app info. + #[serde(flatten)] + pub extra: Map, } impl ListApps for Session { - fn list_apps(&self) { + fn list_apps(&self, list_full: bool, type_filter: Option<&str>) { let resp: ListAppsResponse = self .call( "luna://com.webos.applicationManager/dev/listApps", @@ -33,7 +42,65 @@ impl ListApps for Session { ) .unwrap(); for app in resp.apps { - println!("{}", app.id); + // Mirror the reference CLI: hide non-visible apps (non-signage profile). + if !app.visible.unwrap_or(false) { + continue; + } + if type_filter.is_some_and(|t| app.r#type != t) { + continue; + } + if list_full { + println!("id : {}", app.id); + if let Ok(Value::Object(mut obj)) = serde_json::to_value(&app) { + obj.remove("id"); + print!("{}", convert_json_to_list(&Value::Object(obj), 0)); + } + println!(); + } else { + println!("{}", app.id); + } + } + } +} + +/// Renders a JSON value into an indented text list, matching the reference CLI's +/// `convertJsonToList`. Each nesting level is prefixed with one additional `-`. +fn convert_json_to_list(value: &Value, level: usize) -> String { + let prefix = "-".repeat(level); + let mut out = String::new(); + match value { + Value::String(s) => { + let _ = writeln!(out, "{prefix}{s}"); + } + Value::Array(arr) if !arr.is_empty() => { + for item in arr { + out.push_str(&convert_json_to_list(item, level)); + } } + Value::Object(map) => { + for (key, val) in map { + if is_nested(val) { + let _ = writeln!(out, "{prefix}{key}"); + out.push_str(&convert_json_to_list(val, level + 1)); + } else { + let _ = writeln!(out, "{prefix}{key} : {}", scalar_to_string(val)); + } + } + } + _ => {} + } + out +} + +/// In JavaScript `typeof` reports objects, arrays and `null` all as `"object"`, +/// so the reference implementation recurses into each of these. +fn is_nested(value: &Value) -> bool { + matches!(value, Value::Object(_) | Value::Array(_) | Value::Null) +} + +fn scalar_to_string(value: &Value) -> String { + match value { + Value::String(s) => s.clone(), + other => other.to_string(), } } diff --git a/ares-install/src/main.rs b/ares-install/src/main.rs index a2580a4..ab91029 100644 --- a/ares-install/src/main.rs +++ b/ares-install/src/main.rs @@ -26,6 +26,20 @@ struct Cli { device: Option, #[arg(short, long, group = "action", help = "List the installed apps")] list: bool, + #[arg( + short = 'F', + long = "listfull", + group = "action", + help = "List the installed apps with detailed information" + )] + list_full: bool, + #[arg( + short = 't', + long = "type", + value_name = "APP_TYPE", + help = "Filter the listed apps by APP_TYPE" + )] + app_type: Option, #[arg( short, long, @@ -52,8 +66,8 @@ fn main() { } let device = device.unwrap(); let session = device.new_session().unwrap(); - if cli.list { - session.list_apps(); + if cli.list || cli.list_full { + session.list_apps(cli.list_full, cli.app_type.as_deref()); } else if let Some(id) = cli.remove { println!("Removing {id}..."); match session.remove_app(&id) {