use crate::{
CommandGroup, InvokeCommandInput, InvokeCommandOutput, InvokeCommandResult, InvokeReport, InvokeReportField, InvokeReportTable,
InvokeReportValue, invoke_command,
};
use auv_cli_common::{TableRow, outputs::formats::table::TableOptions};
use clap::Args;
use auv_tracing::ArtifactMetadata;
use crate::artifact::emit_png_with_receipt;
#[cfg(target_os = "macos")]
use auv_driver::overlay::{Overlay, components::CaptureFrame};
#[cfg(target_os = "macos")]
use std::time::Duration;
pub fn group() -> CommandGroup {
CommandGroup::new("display", "DISPLAY").command(capture_display_invoke_command()).command(list_displays_invoke_command())
}
#[derive(Clone, Debug, Args, serde::Serialize, serde::Deserialize)]
#[command(after_long_help = "Examples:\n auv invoke display.capture")]
struct CaptureDisplayArgs {}
#[invoke_command(
id = "display.capture",
group = "display",
description = "Capture the primary display with its screenshot-to-logical coordinate contract.",
input = CaptureDisplayArgs,
)]
async fn capture_display(input: InvokeCommandInput, _args: CaptureDisplayArgs) -> InvokeCommandResult {
if input.dry_run {
return Ok(InvokeCommandOutput::completed());
}
#[cfg(target_os = "macos")]
{
let session = auv::local::open().map_err(|error| error.to_string())?;
let (result, artifact) = capture_primary_display_recorded_with_session(&session).await?;
let capture_overlay = Overlay::new().with_layer(
CaptureFrame::new(result.display.frame)
.with_label(result.display.name.clone().unwrap_or_else(|| format!("display {}", result.display.id))),
);
let overlay = super::overlay::show_overlay(
&input,
&session,
capture_overlay,
auv_driver::overlay::ShowOptions::new()
.with_motion_ease(Duration::from_millis(120), auv_driver::overlay::Easing::EaseInOutExpo)
.with_auto_removal_after(Duration::from_millis(180)),
)?;
let mut output = display_capture_output(&result, artifact)?;
output.report.as_mut().expect("display capture output always has a report").fields.push(overlay.report_field());
Ok(output)
}
#[cfg(target_os = "linux")]
{
let session = auv::local::open().map_err(|error| error.to_string())?;
let (result, artifact) = capture_primary_display_recorded_with_session(&session).await?;
display_capture_output(&result, artifact)
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err("display.capture is not available on this platform".to_string())
}
}
pub async fn recorded_display_capture_output(result: &auv_driver::DisplayCapture) -> InvokeCommandResult {
let artifact = emit_png_with_receipt("auv.driver.display_capture", &result.capture.image).await;
display_capture_output(result, artifact)
}
fn display_capture_output(result: &auv_driver::DisplayCapture, artifact: Option<ArtifactMetadata>) -> InvokeCommandResult {
Ok(
InvokeCommandOutput::from_result(&super::display_capture_result(&result.display, &result.capture))?
.with_report(display_capture_report(result))
.with_artifacts(artifact),
)
}
pub async fn capture_primary_display() -> Result<auv_driver::DisplayCapture, String> {
capture_primary_display_recorded().await.map(|(capture, _)| capture)
}
async fn capture_primary_display_recorded() -> Result<(auv_driver::DisplayCapture, Option<ArtifactMetadata>), String> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let session = auv::local::open().map_err(|error| error.to_string())?;
capture_primary_display_recorded_with_session(&session).await
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err("display.capture is not available on this platform".to_string())
}
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
async fn capture_primary_display_recorded_with_session(
session: &auv_driver::LocalDriverSession,
) -> Result<(auv_driver::DisplayCapture, Option<ArtifactMetadata>), String> {
let result = session.display().capture(auv_driver::CaptureOptions::default()).map_err(|error| error.to_string())?;
let artifact = emit_png_with_receipt("auv.driver.display_capture", &result.capture.image).await;
Ok((result, artifact))
}
#[derive(Clone, Debug, Args, serde::Serialize, serde::Deserialize)]
#[command(after_long_help = "Examples:\n auv invoke display.list --json")]
struct ListDisplaysArgs {}
#[invoke_command(
id = "display.list",
group = "display",
description = "List connected displays using the normalized AUV coordinate contract.",
input = ListDisplaysArgs,
)]
async fn list_displays(input: InvokeCommandInput, _args: ListDisplaysArgs) -> InvokeCommandResult {
if input.dry_run {
return Ok(InvokeCommandOutput::completed());
}
let displays = observe_displays().await?;
list_displays_output(&displays)
}
pub fn list_displays_output(displays: &auv_driver::ObservedDisplays) -> InvokeCommandResult {
Ok(InvokeCommandOutput::from_result(displays)?.with_report(display_list_report(&displays.displays)))
}
pub async fn observe_displays() -> Result<auv_driver::ObservedDisplays, String> {
#[cfg(any(target_os = "linux", target_os = "macos"))]
{
let session = auv::local::open().map_err(|error| error.to_string())?;
session.display().list().map_err(|error| error.to_string())
}
#[cfg(not(any(target_os = "linux", target_os = "macos")))]
{
Err("display.list is not available on this platform".to_string())
}
}
fn display_capture_report(result: &auv_driver::DisplayCapture) -> InvokeReport {
let mut fields = vec![
InvokeReportField::new("Display", result.display.name.clone().unwrap_or_else(|| format!("display {}", result.display.id))),
InvokeReportField::new("Display ID", result.display.id.clone()),
InvokeReportField::new("Display frame", result.display.frame.report_value()),
InvokeReportField::new("Capture bounds", result.capture.bounds.report_value()),
InvokeReportField::new("Pixel size", format!("{}x{}", result.capture.image.width(), result.capture.image.height())),
InvokeReportField::new("Scale factor", format!("{:.3}", result.capture.scale_factor)),
];
if let Some(reason) = result.capture.fallback_reason.as_deref() {
fields.push(InvokeReportField::new("Fallback reason", reason));
}
InvokeReport::new(fields, Vec::new())
}
#[derive(TableRow)]
struct DisplayRow {
#[table(header = "REF")]
reference: String,
role: &'static str,
name: String,
frame: String,
#[table(display_with = |scale: &f64| format!("{scale:.3}"))]
scale: f64,
#[table(wide)]
kind: &'static str,
}
fn display_list_report(displays: &[auv_driver::Display]) -> InvokeReport {
let rows = displays
.iter()
.map(|display| DisplayRow {
reference: display.id.clone(),
role: if display.is_primary {
"primary"
} else {
"secondary"
},
name: display.name.clone().unwrap_or_else(|| format!("display {}", display.id)),
frame: display.frame.report_value(),
scale: display.scale_factor,
kind: match display.is_builtin {
Some(true) => "built-in",
Some(false) => "external",
None => "unknown",
},
})
.collect::<Vec<_>>();
InvokeReport {
fields: vec![InvokeReportField::new(
"Result",
format!("{} display(s)", displays.len()),
)],
tables: vec![InvokeReportTable::from_rows(&rows, TableOptions::default())],
wide_tables: vec![InvokeReportTable::from_rows(
&rows,
TableOptions::default().wide(true),
)],
sections: Vec::new(),
}
}
#[cfg(test)]
#[path = "display_test.rs"]
mod tests;