virtuoso_cli/commands/
window.rs1use crate::client::bridge::VirtuosoClient;
2use crate::error::{Result, VirtuosoError};
3use serde_json::{json, Value};
4
5pub fn list() -> Result<Value> {
12 let client = VirtuosoClient::from_env()?;
13 let r = client.execute_skill(&client.window.list_windows(), None)?;
14 if !r.skill_ok() {
15 return Err(VirtuosoError::Execution(format!(
16 "failed to list windows: {}",
17 r.errors.join("; ")
18 )));
19 }
20 let windows = parse_window_json(&r.output);
21 let windows = annotate_modes(windows);
23 Ok(json!({ "windows": windows }))
24}
25
26pub fn dismiss_dialog(action: &str, dry_run: bool) -> Result<Value> {
32 let client = VirtuosoClient::from_env()?;
33 if dry_run {
34 let r = client.execute_skill(&client.window.get_dialog_info(), None)?;
35 let raw = r.output.trim_matches('"');
36 let active = r.skill_ok() && raw != "no-dialog";
37 return Ok(json!({
38 "dialog": if active { raw } else { "none" },
39 "active": active,
40 "dry_run": true,
41 }));
42 }
43 let r = client.execute_skill(&client.window.dismiss_dialog(action), None)?;
44 let dismissed = r.skill_ok() && r.output.trim_matches('"') != "no-dialog";
45 Ok(json!({
46 "status": if dismissed { "dismissed" } else { "no-dialog" },
47 "action": action,
48 }))
49}
50
51pub fn screenshot(path: &str, window_pattern: Option<&str>) -> Result<Value> {
55 let client = VirtuosoClient::from_env()?;
56 let skill = match window_pattern {
57 Some(pat) => client.window.screenshot_by_pattern(path, pat),
58 None => client.window.screenshot(path),
59 };
60 let r = client.execute_skill(&skill, None)?;
61 if !r.skill_ok() {
62 let detail = if r.output.is_empty() {
63 r.errors.join("; ")
64 } else {
65 r.output.clone()
66 };
67 return Err(VirtuosoError::Execution(format!(
68 "screenshot failed: {}",
69 detail
70 )));
71 }
72 if r.output.trim_matches('"') == "no-match" {
73 return Err(VirtuosoError::NotFound(format!(
74 "no window matching pattern '{}'",
75 window_pattern.unwrap_or("")
76 )));
77 }
78 Ok(json!({
79 "status": "saved",
80 "path": path,
81 }))
82}
83
84fn window_mode(name: &str) -> &'static str {
86 if name.contains("ADE Explorer Editing") || name.contains("ADE Assembler Editing") {
87 "ade-editing"
88 } else if name.contains("ADE Explorer Reading") {
89 "ade-reading"
90 } else if name.contains("ADE") {
91 "ade-other"
92 } else if name.contains("Schematic Editor") {
93 "schematic"
94 } else if name.contains("Layout Editor") {
95 "layout"
96 } else {
97 "other"
98 }
99}
100
101fn parse_window_json(output: &str) -> Value {
106 let s = output.trim_matches('"');
108 let decoded = decode_skill_octal(s);
110 let unescaped = decoded.replace("\\\"", "\"").replace("\\\\", "\\");
111 serde_json::from_str(&unescaped).unwrap_or_else(|_| json!([]))
112}
113
114fn decode_skill_octal(s: &str) -> String {
117 let bytes = s.as_bytes();
118 let mut out = String::with_capacity(s.len());
119 let mut i = 0;
120 while i < bytes.len() {
121 if bytes[i] == b'\\' && i + 1 < bytes.len() && bytes[i + 1].is_ascii_digit() {
122 let start = i + 1;
124 let mut end = start;
125 while end < bytes.len() && end < start + 3 && bytes[end].is_ascii_digit() {
126 end += 1;
127 }
128 if let Ok(octal_str) = std::str::from_utf8(&bytes[start..end]) {
129 if let Ok(n) = u32::from_str_radix(octal_str, 8) {
130 if let Some(c) = char::from_u32(n) {
131 out.push(c);
132 i = end;
133 continue;
134 }
135 }
136 }
137 }
138 out.push(bytes[i] as char);
139 i += 1;
140 }
141 out
142}
143
144fn annotate_modes(v: Value) -> Value {
145 match v {
146 Value::Array(arr) => Value::Array(
147 arr.into_iter()
148 .map(|mut item| {
149 if let Some(name) = item.get("name").and_then(|n| n.as_str()) {
150 let mode = window_mode(name).to_string();
151 item.as_object_mut()
152 .map(|o| o.insert("mode".into(), json!(mode)));
153 }
154 item
155 })
156 .collect(),
157 ),
158 other => other,
159 }
160}