Skip to main content

virtuoso_cli/commands/
window.rs

1use crate::client::bridge::VirtuosoClient;
2use crate::error::{Result, VirtuosoError};
3use serde_json::{json, Value};
4
5/// List all open Virtuoso windows with their names.
6///
7/// Window names reveal the current mode, e.g.:
8///   "ADE Explorer Editing: LIB/CELL/maestro"
9///   "ADE Explorer Reading: ..."
10///   "Virtuoso Schematic Editor"
11pub 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    // Annotate each window with a derived mode field
22    let windows = annotate_modes(windows);
23    Ok(json!({ "windows": windows }))
24}
25
26/// Dismiss the currently active blocking dialog.
27///
28/// With --dry-run, reports the dialog name without clicking anything.
29/// action "cancel" (default): clicks Cancel / closes dialog.
30/// action "ok": attempts hiSendOK — may not be supported by all dialog types.
31pub 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
51/// Capture a screenshot of the current (or pattern-matched) Virtuoso window.
52///
53/// Saves to --path as PNG. Requires IC23.1+ (hiGetWindowScreenDump).
54pub 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
84/// Derive a mode string from a Virtuoso window name.
85fn 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
101/// Parse the JSON string returned by list_windows().
102///
103/// SKILL encodes non-ASCII chars as octal escapes (e.g. `\256` = ®).
104/// Standard JSON parsers reject these, so we decode them first.
105fn parse_window_json(output: &str) -> Value {
106    // Strip surrounding SKILL string quotes
107    let s = output.trim_matches('"');
108    // Decode SKILL octal escapes (\NNN) → UTF-8, then un-escape \" and \\
109    let decoded = decode_skill_octal(s);
110    let unescaped = decoded.replace("\\\"", "\"").replace("\\\\", "\\");
111    serde_json::from_str(&unescaped).unwrap_or_else(|_| json!([]))
112}
113
114/// Convert SKILL's `\NNN` octal escapes to their UTF-8 codepoints.
115/// Leaves other backslash sequences untouched (they are handled later).
116fn 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            // Collect up to 3 octal digits
123            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}