open_sesame/core/
window.rs

1//! Window domain types
2//!
3//! NewTypes for window-related identifiers to provide type safety.
4
5use std::fmt;
6
7/// Unique identifier for a window (from Wayland protocol)
8///
9/// Wraps a string identifier obtained from the window manager.
10/// On Wayland/COSMIC, this is typically a handle from the toplevel manager protocol.
11///
12/// # Examples
13///
14/// ```
15/// use open_sesame::WindowId;
16///
17/// let id = WindowId::new("cosmic-toplevel-123");
18/// assert_eq!(id.as_str(), "cosmic-toplevel-123");
19/// ```
20#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub struct WindowId(String);
22
23impl WindowId {
24    /// Create a new WindowId from a string identifier
25    pub fn new(id: impl Into<String>) -> Self {
26        Self(id.into())
27    }
28
29    /// Get the underlying string identifier
30    pub fn as_str(&self) -> &str {
31        &self.0
32    }
33}
34
35impl fmt::Display for WindowId {
36    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37        write!(f, "{}", self.0)
38    }
39}
40
41impl From<String> for WindowId {
42    fn from(s: String) -> Self {
43        Self::new(s)
44    }
45}
46
47impl From<&str> for WindowId {
48    fn from(s: &str) -> Self {
49        Self::new(s)
50    }
51}
52
53/// Application identifier (e.g., "firefox", "com.mitchellh.ghostty")
54///
55/// Represents an application's unique identifier, typically following
56/// reverse-DNS notation on Linux/Wayland systems.
57///
58/// # Examples
59///
60/// ```
61/// use open_sesame::AppId;
62///
63/// let app_id = AppId::new("com.mitchellh.ghostty");
64/// assert_eq!(app_id.as_str(), "com.mitchellh.ghostty");
65/// assert_eq!(app_id.last_segment(), "ghostty");
66///
67/// let simple = AppId::new("firefox");
68/// assert_eq!(simple.last_segment(), "firefox");
69/// ```
70#[derive(Debug, Clone, PartialEq, Eq, Hash)]
71pub struct AppId(String);
72
73impl AppId {
74    /// Create a new AppId
75    pub fn new(id: impl Into<String>) -> Self {
76        Self(id.into())
77    }
78
79    /// Get the underlying string
80    pub fn as_str(&self) -> &str {
81        &self.0
82    }
83
84    /// Returns the last segment of a dotted app ID.
85    ///
86    /// For example, "com.mitchellh.ghostty" returns "ghostty".
87    pub fn last_segment(&self) -> &str {
88        self.0.split('.').next_back().unwrap_or(&self.0)
89    }
90
91    /// Returns a lowercase version for case-insensitive comparison.
92    pub fn to_lowercase(&self) -> String {
93        self.0.to_lowercase()
94    }
95}
96
97impl fmt::Display for AppId {
98    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99        write!(f, "{}", self.0)
100    }
101}
102
103impl From<String> for AppId {
104    fn from(s: String) -> Self {
105        Self::new(s)
106    }
107}
108
109impl From<&str> for AppId {
110    fn from(s: &str) -> Self {
111        Self::new(s)
112    }
113}
114
115/// A window on the desktop
116///
117/// Represents a toplevel window obtained from the window manager.
118///
119/// # Examples
120///
121/// ```
122/// use open_sesame::Window;
123///
124/// let window = Window::new(
125///     "toplevel-1",
126///     "firefox",
127///     "GitHub - Mozilla Firefox"
128/// );
129///
130/// assert_eq!(window.app_id.as_str(), "firefox");
131/// assert_eq!(window.title, "GitHub - Mozilla Firefox");
132/// assert!(!window.is_focused);
133///
134/// let focused = Window::with_focus(
135///     "toplevel-2",
136///     "ghostty",
137///     "Terminal",
138///     true
139/// );
140/// assert!(focused.is_focused);
141/// ```
142#[derive(Debug, Clone)]
143pub struct Window {
144    /// Unique identifier for activation
145    pub id: WindowId,
146    /// Application identifier
147    pub app_id: AppId,
148    /// Window title
149    pub title: String,
150    /// Whether this window currently has focus
151    pub is_focused: bool,
152}
153
154impl Window {
155    /// Create a new window
156    pub fn new(
157        id: impl Into<WindowId>,
158        app_id: impl Into<AppId>,
159        title: impl Into<String>,
160    ) -> Self {
161        Self {
162            id: id.into(),
163            app_id: app_id.into(),
164            title: title.into(),
165            is_focused: false,
166        }
167    }
168
169    /// Create a new window with focus state
170    pub fn with_focus(
171        id: impl Into<WindowId>,
172        app_id: impl Into<AppId>,
173        title: impl Into<String>,
174        is_focused: bool,
175    ) -> Self {
176        Self {
177            id: id.into(),
178            app_id: app_id.into(),
179            title: title.into(),
180            is_focused,
181        }
182    }
183
184    /// Create a mock window for testing
185    #[cfg(test)]
186    pub fn mock(app_id: &str, title: &str) -> Self {
187        Self::new(format!("mock-{}-{}", app_id, title.len()), app_id, title)
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194
195    #[test]
196    fn test_window_id() {
197        let id = WindowId::new("test-123");
198        assert_eq!(id.as_str(), "test-123");
199        assert_eq!(format!("{}", id), "test-123");
200    }
201
202    #[test]
203    fn test_app_id_last_segment() {
204        let app = AppId::new("com.mitchellh.ghostty");
205        assert_eq!(app.last_segment(), "ghostty");
206
207        let simple = AppId::new("firefox");
208        assert_eq!(simple.last_segment(), "firefox");
209    }
210
211    #[test]
212    fn test_window_creation() {
213        let window = Window::new("id-1", "firefox", "GitHub - Mozilla Firefox");
214        assert_eq!(window.id.as_str(), "id-1");
215        assert_eq!(window.app_id.as_str(), "firefox");
216        assert_eq!(window.title, "GitHub - Mozilla Firefox");
217    }
218}