Skip to main content

smcp_computer/desktop/
window_uri.rs

1/*!
2* 文件名: window_uri
3* 作者: JQQ
4* 创建日期: 2025/12/18
5* 最后修改日期: 2025/12/18
6* 版权: 2023 JQQ. All rights reserved.
7* 依赖: url, serde
8* 描述: Window URI 解析与处理,对应 Python 侧的 window_uri.py
9*/
10
11use std::collections::HashMap;
12use url::Url;
13
14/// Window URI 解析器 / Window URI parser
15/// 对应 Python 侧的 WindowURI 类
16#[derive(Debug, Clone)]
17pub struct WindowURI {
18    /// 原始 URL / Original URL
19    url: Url,
20    /// 缓存的路径段 / Cached path segments
21    windows: Vec<String>,
22    /// 缓存的查询参数 / Cached query parameters
23    params: HashMap<String, String>,
24}
25
26impl WindowURI {
27    /// 创建新的 WindowURI / Create new WindowURI
28    pub fn new(uri: &str) -> Result<Self, WindowURIError> {
29        let url = Url::parse(uri)
30            .map_err(|e| WindowURIError::InvalidURI(format!("Failed to parse URI: {}", e)))?;
31
32        if url.scheme() != "window" {
33            return Err(WindowURIError::InvalidScheme(url.scheme().to_string()));
34        }
35
36        if url.host().is_none() || url.host_str().unwrap().is_empty() {
37            return Err(WindowURIError::MissingHost);
38        }
39
40        // 解析路径段 / Parse path segments
41        let windows = url
42            .path()
43            .trim_start_matches('/')
44            .split('/')
45            .filter(|s| !s.is_empty())
46            .map(|s| {
47                percent_encoding::percent_decode_str(s)
48                    .decode_utf8()
49                    .map(|s| s.to_string())
50                    .map_err(|e| {
51                        WindowURIError::InvalidPath(format!("Failed to decode path segment: {}", e))
52                    })
53            })
54            .collect::<Result<Vec<_>, _>>()?;
55
56        // 解析查询参数 / Parse query parameters
57        let params: HashMap<String, String> = url.query_pairs().into_owned().collect();
58
59        // 验证查询参数 / Validate query parameters
60        let uri = Self {
61            url,
62            windows,
63            params,
64        };
65
66        // 验证 priority / Validate priority
67        if let Some(priority) = uri.priority() {
68            if !(0..=100).contains(&priority) {
69                return Err(WindowURIError::InvalidPriority(priority));
70            }
71        }
72
73        // 验证 fullscreen / Validate fullscreen
74        let _ = uri.fullscreen(); // Will error if invalid
75
76        Ok(uri)
77    }
78
79    /// 获取 MCP ID (host) / Get MCP ID (host)
80    pub fn mcp_id(&self) -> &str {
81        self.url.host_str().unwrap()
82    }
83
84    /// 获取窗口路径列表 / Get window path list
85    pub fn windows(&self) -> &[String] {
86        &self.windows
87    }
88
89    /// 获取优先级 / Get priority (0-100)
90    pub fn priority(&self) -> Option<i32> {
91        self.params.get("priority").and_then(|s| s.parse().ok())
92    }
93
94    /// 获取全屏标志 / Get fullscreen flag
95    pub fn fullscreen(&self) -> Option<bool> {
96        self.params
97            .get("fullscreen")
98            .and_then(|s| match s.to_lowercase().as_str() {
99                "1" | "true" | "yes" | "on" => Some(true),
100                "0" | "false" | "no" | "off" => Some(false),
101                _ => None,
102            })
103    }
104
105    /// 构建 Window URI / Build Window URI
106    pub fn build(
107        host: &str,
108        windows: &[String],
109        priority: Option<i32>,
110        fullscreen: Option<bool>,
111    ) -> Result<String, WindowURIError> {
112        if host.is_empty() {
113            return Err(WindowURIError::MissingHost);
114        }
115
116        let mut url = Url::parse(&format!("window://{}", host))
117            .map_err(|e| WindowURIError::InvalidURI(format!("Failed to build URI: {}", e)))?;
118
119        // 添加路径段 / Add path segments
120        if !windows.is_empty() {
121            let encoded_path: Vec<String> = windows
122                .iter()
123                .map(|w| {
124                    percent_encoding::utf8_percent_encode(w, percent_encoding::NON_ALPHANUMERIC)
125                        .to_string()
126                })
127                .collect();
128            url.set_path(&encoded_path.join("/"));
129        }
130
131        // 添加查询参数 / Add query parameters
132        let mut query_pairs = Vec::new();
133
134        if let Some(p) = priority {
135            if !(0..=100).contains(&p) {
136                return Err(WindowURIError::InvalidPriority(p));
137            }
138            query_pairs.push(("priority", p.to_string()));
139        }
140
141        if let Some(f) = fullscreen {
142            query_pairs.push(("fullscreen", if f { "true" } else { "false" }.to_string()));
143        }
144
145        if !query_pairs.is_empty() {
146            url.set_query(Some(
147                &query_pairs
148                    .iter()
149                    .map(|(k, v)| format!("{}={}", k, v))
150                    .collect::<Vec<_>>()
151                    .join("&"),
152            ));
153        }
154
155        Ok(url.to_string())
156    }
157}
158
159impl std::fmt::Display for WindowURI {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        write!(f, "{}", self.url)
162    }
163}
164
165/// Window URI 错误 / Window URI errors
166#[derive(Debug, thiserror::Error)]
167pub enum WindowURIError {
168    #[error("Invalid URI: {0}")]
169    InvalidURI(String),
170
171    #[error("Invalid scheme: {0}, expected 'window'")]
172    InvalidScheme(String),
173
174    #[error("Missing host (MCP ID)")]
175    MissingHost,
176
177    #[error("Invalid path: {0}")]
178    InvalidPath(String),
179
180    #[error("Invalid priority: {0}, must be between 0 and 100")]
181    InvalidPriority(i32),
182
183    #[error("Invalid fullscreen value")]
184    InvalidFullscreen,
185}
186
187/// 检查是否为有效的 Window URI / Check if URI is a valid Window URI
188pub fn is_window_uri(uri: &str) -> bool {
189    WindowURI::new(uri).is_ok()
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_parse_minimal() {
198        let uri = WindowURI::new("window://com.example.mcp").unwrap();
199        assert_eq!(uri.mcp_id(), "com.example.mcp");
200        assert!(uri.windows().is_empty());
201        assert_eq!(uri.priority(), None);
202        assert_eq!(uri.fullscreen(), None);
203    }
204
205    #[test]
206    fn test_parse_with_paths() {
207        let uri = WindowURI::new("window://com.example.mcp/dashboard/main").unwrap();
208        assert_eq!(uri.mcp_id(), "com.example.mcp");
209        assert_eq!(uri.windows(), &["dashboard", "main"]);
210    }
211
212    #[test]
213    fn test_parse_with_query_params() {
214        let uri =
215            WindowURI::new("window://com.example.mcp/page?priority=90&fullscreen=true").unwrap();
216        assert_eq!(uri.windows(), &["page"]);
217        assert_eq!(uri.priority(), Some(90));
218        assert_eq!(uri.fullscreen(), Some(true));
219    }
220
221    #[test]
222    fn test_priority_bounds() {
223        assert!(WindowURI::new("window://x?priority=0").is_ok());
224        assert!(WindowURI::new("window://x?priority=100").is_ok());
225        assert!(WindowURI::new("window://x?priority=-1").is_err());
226        assert!(WindowURI::new("window://x?priority=101").is_err());
227    }
228
229    #[test]
230    fn test_fullscreen_variants() {
231        let test_cases = vec![
232            ("true", true),
233            ("1", true),
234            ("yes", true),
235            ("on", true),
236            ("false", false),
237            ("0", false),
238            ("no", false),
239            ("off", false),
240        ];
241
242        for (val, expected) in test_cases {
243            let uri = WindowURI::new(&format!("window://x?fullscreen={}", val)).unwrap();
244            assert_eq!(uri.fullscreen(), Some(expected));
245        }
246    }
247
248    #[test]
249    fn test_build_uri() {
250        let uri = WindowURI::build(
251            "com.example.mcp",
252            &["dashboard".to_string(), "main".to_string()],
253            Some(80),
254            Some(false),
255        )
256        .unwrap();
257
258        assert!(uri.starts_with("window://com.example.mcp/dashboard/main"));
259        assert!(uri.contains("priority=80"));
260        assert!(uri.contains("fullscreen=false"));
261    }
262
263    #[test]
264    fn test_is_window_uri() {
265        assert!(is_window_uri("window://com.example.mcp"));
266        assert!(is_window_uri("window://host/path?priority=50"));
267        assert!(!is_window_uri("http://example.com"));
268        assert!(!is_window_uri("window://"));
269    }
270}