use ff_rdp_core::{ActorId, DomWalkerActor, InspectorActor, PageStyleActor};
use serde_json::{Value, json};
use crate::cli::args::Cli;
use crate::error::AppError;
use crate::hints::{HintContext, HintSource};
use crate::output;
use crate::output_controls::{OutputControls, SortDir};
use crate::output_pipeline::OutputPipeline;
use super::connect_tab::{ConnectedTab, connect_and_get_target};
fn setup(cli: &Cli, selector: &str) -> Result<(ConnectedTab, ActorId, ActorId), AppError> {
let mut ctx = connect_and_get_target(cli)?;
let inspector_actor = ctx
.target
.inspector_actor
.clone()
.ok_or_else(|| AppError::User("no inspector actor available".to_string()))?;
let walker_actor = InspectorActor::get_walker(ctx.transport_mut(), &inspector_actor)
.map_err(map_style_error)?;
let page_style_actor = InspectorActor::get_page_style(ctx.transport_mut(), &inspector_actor)
.map_err(map_style_error)?;
let doc_root = DomWalkerActor::document_element(ctx.transport_mut(), &walker_actor)
.map_err(map_style_error)?;
let root_actor_str = doc_root
.actor
.as_deref()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("document root node has no actor ID")))?;
let root_actor = ActorId::from(root_actor_str);
let maybe_node =
DomWalkerActor::query_selector(ctx.transport_mut(), &walker_actor, &root_actor, selector)
.map_err(map_style_error)?;
let node = maybe_node
.ok_or_else(|| AppError::User(format!("no element matching selector '{selector}'")))?;
let node_actor_str = node
.actor
.as_deref()
.ok_or_else(|| AppError::Internal(anyhow::anyhow!("matched node has no actor ID")))?;
let node_actor = ActorId::from(node_actor_str);
Ok((ctx, page_style_actor, node_actor))
}
pub fn run(cli: &Cli, selector: &str, properties: Option<&[String]>) -> Result<(), AppError> {
let (mut ctx, page_style_actor, node_actor) = setup(cli, selector)?;
let computed =
PageStyleActor::get_computed(ctx.transport_mut(), &page_style_actor, &node_actor)
.map_err(map_style_error)?;
let mut items: Vec<Value> = computed
.iter()
.map(|p| {
json!({
"name": p.name,
"value": p.value,
"priority": p.priority,
})
})
.collect();
if let Some(props) = properties {
items.retain(|item| {
item.get("name")
.and_then(Value::as_str)
.is_some_and(|name| props.iter().any(|p| p == name))
});
}
let controls = OutputControls::from_cli(cli, SortDir::Asc);
if cli.sort.is_some() {
controls.apply_sort(&mut items);
}
let items = controls.apply_fields(items);
let (items, total, truncated) = controls.apply_limit(items, None);
let shown = items.len();
let results = Value::Array(items);
let mut meta = json!({
"host": cli.host,
"port": cli.port,
"selector": selector,
});
crate::connection_meta::merge_into(&mut meta, &cli.host, cli.port, None);
let envelope = output::envelope_with_truncation(&results, shown, total, truncated, &meta);
let hint_ctx = HintContext::new(HintSource::Styles).with_selector(selector);
OutputPipeline::from_cli(cli)?
.finalize_with_hints(&envelope, Some(&hint_ctx))
.map_err(AppError::from)
}
pub fn run_applied(cli: &Cli, selector: &str) -> Result<(), AppError> {
let (mut ctx, page_style_actor, node_actor) = setup(cli, selector)?;
let applied = PageStyleActor::get_applied(ctx.transport_mut(), &page_style_actor, &node_actor)
.map_err(map_style_error)?;
let mut items: Vec<Value> = applied
.iter()
.map(|r| serde_json::to_value(r).map_err(|e| AppError::Internal(e.into())))
.collect::<Result<Vec<_>, _>>()?;
let controls = OutputControls::from_cli(cli, SortDir::Asc);
if cli.sort.is_none() {
items.sort_by(|a, b| {
let sa = a.get("selector").and_then(Value::as_str).unwrap_or("");
let sb = b.get("selector").and_then(Value::as_str).unwrap_or("");
sa.cmp(sb)
});
} else {
controls.apply_sort(&mut items);
}
let items = controls.apply_fields(items);
let (items, total, truncated) = controls.apply_limit(items, None);
let shown = items.len();
let results = Value::Array(items);
let mut meta = json!({
"host": cli.host,
"port": cli.port,
"selector": selector,
});
crate::connection_meta::merge_into(&mut meta, &cli.host, cli.port, None);
let envelope = output::envelope_with_truncation(&results, shown, total, truncated, &meta);
let hint_ctx = HintContext::new(HintSource::Styles).with_selector(selector);
OutputPipeline::from_cli(cli)?
.finalize_with_hints(&envelope, Some(&hint_ctx))
.map_err(AppError::from)
}
pub fn run_layout(cli: &Cli, selector: &str) -> Result<(), AppError> {
let (mut ctx, page_style_actor, node_actor) = setup(cli, selector)?;
let layout = PageStyleActor::get_layout(ctx.transport_mut(), &page_style_actor, &node_actor)
.map_err(map_style_error)?;
let results = serde_json::to_value(&layout).map_err(|e| AppError::Internal(e.into()))?;
let mut meta = json!({
"host": cli.host,
"port": cli.port,
"selector": selector,
});
crate::connection_meta::merge_into(&mut meta, &cli.host, cli.port, None);
let envelope = output::envelope(&results, 1, &meta);
let hint_ctx = HintContext::new(HintSource::Styles).with_selector(selector);
OutputPipeline::from_cli(cli)?
.finalize_with_hints(&envelope, Some(&hint_ctx))
.map_err(AppError::from)
}
fn map_style_error(err: ff_rdp_core::ProtocolError) -> AppError {
match &err {
ff_rdp_core::ProtocolError::ActorError { error, .. }
if error == "noSuchActor" || error == "unknownActor" =>
{
AppError::User(
"style actor is no longer valid — the actor may have expired after navigation. \
Re-run the command to get a fresh actor"
.to_string(),
)
}
_ => AppError::from(err),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn map_style_error_no_such_actor() {
let err = ff_rdp_core::ProtocolError::ActorError {
actor: "conn0/pageStyleActor1".to_string(),
kind: ff_rdp_core::ActorErrorKind::UnknownActor,
error: "noSuchActor".to_string(),
message: "actor not found".to_string(),
};
let app_err = map_style_error(err);
match app_err {
AppError::User(msg) => assert!(msg.contains("no longer valid")),
other => panic!("expected User error, got {other:?}"),
}
}
#[test]
fn map_style_error_unknown_actor() {
let err = ff_rdp_core::ProtocolError::ActorError {
actor: "conn0/pageStyleActor1".to_string(),
kind: ff_rdp_core::ActorErrorKind::UnknownActor,
error: "unknownActor".to_string(),
message: String::new(),
};
let app_err = map_style_error(err);
match app_err {
AppError::User(msg) => assert!(msg.contains("no longer valid")),
other => panic!("expected User error, got {other:?}"),
}
}
#[test]
fn map_style_error_other_errors_become_internal() {
let err = ff_rdp_core::ProtocolError::InvalidPacket("bad data".into());
let app_err = map_style_error(err);
match app_err {
AppError::Internal(_) => {}
other => panic!("expected Internal error, got {other:?}"),
}
}
#[test]
fn computed_items_serialise_correctly() {
let item = json!({
"name": "color",
"value": "rgb(0, 0, 0)",
"priority": "",
});
assert_eq!(item["name"], "color");
assert_eq!(item["value"], "rgb(0, 0, 0)");
assert_eq!(item["priority"], "");
}
fn make_items() -> Vec<Value> {
vec![
json!({"name": "color", "value": "rgb(0,0,0)", "priority": ""}),
json!({"name": "display", "value": "block", "priority": ""}),
json!({"name": "font-size", "value": "16px", "priority": ""}),
json!({"name": "margin-top", "value": "0px", "priority": ""}),
]
}
fn apply_properties_filter(mut items: Vec<Value>, props: Option<&[String]>) -> Vec<Value> {
if let Some(filter) = props {
items.retain(|item| {
item.get("name")
.and_then(Value::as_str)
.is_some_and(|name| filter.iter().any(|p| p == name))
});
}
items
}
#[test]
fn properties_filter_none_returns_all() {
let items = make_items();
let result = apply_properties_filter(items, None);
assert_eq!(result.len(), 4);
}
#[test]
fn properties_filter_single_property() {
let items = make_items();
let props = vec!["color".to_string()];
let result = apply_properties_filter(items, Some(&props));
assert_eq!(result.len(), 1);
assert_eq!(result[0]["name"], "color");
}
#[test]
fn properties_filter_multiple_properties() {
let items = make_items();
let props = vec!["color".to_string(), "display".to_string()];
let result = apply_properties_filter(items, Some(&props));
assert_eq!(result.len(), 2);
let names: Vec<&str> = result.iter().filter_map(|i| i["name"].as_str()).collect();
assert!(names.contains(&"color"));
assert!(names.contains(&"display"));
}
#[test]
fn properties_filter_unknown_property_returns_empty() {
let items = make_items();
let props = vec!["nonexistent-prop".to_string()];
let result = apply_properties_filter(items, Some(&props));
assert!(result.is_empty());
}
}