1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
// windows_api.rs — Windows API bindings for showit
// Author: Hadi Cahyadi <cumulus13@gmail.com>
//
// On non-Windows platforms this module provides stub implementations
// so the crate can still compile and tests can run on Linux/macOS CI.
#[derive(Debug, Clone)]
pub struct WindowInfo {
pub hwnd: usize, // opaque handle, stored as usize for portability
pub title: String,
/// Executable name of the owning process, e.g. "wt.exe", "Code.exe".
/// Empty string if it could not be determined.
pub process_name: String,
/// Win32 window class name, e.g. "CASCADIA_HOSTING_WINDOW_CLASS".
/// Empty string if it could not be determined.
pub class_name: String,
}
impl WindowInfo {
/// A short human-readable "type" label to display instead of (or alongside)
/// the raw title when the title alone does not identify the app reliably.
///
/// Rules (first match wins):
/// 1. Known class names that map to a canonical app label.
/// 2. Known process names (exe stem, lower-case) that map to a label.
/// 3. Fall back to the exe stem with the first letter capitalised.
/// 4. Empty string if both process_name and class_name are unknown.
pub fn type_label(&self) -> String {
let class_lc = self.class_name.to_lowercase();
let proc_stem = exe_stem(&self.process_name);
// ── Class-based identification ────────────────────────────────────────
// These window classes are stable identifiers regardless of title.
let by_class: Option<&str> = match class_lc.as_str() {
"cascadia_hosting_window_class" => Some("Windows Terminal"),
"chrome_widgetwin_1" => Some("Chrome"),
"mozillawindowclass" => Some("Firefox"),
"operawindowclass" => Some("Opera"),
"bravebrowser" => Some("Brave"),
"vscode_main_window" => Some("VS Code"),
"notepad++" => Some("Notepad++"),
"konsole_mainwindow" => Some("Konsole"),
"gnome-terminal-window" => Some("GNOME Terminal"),
_ => None,
};
if let Some(label) = by_class {
return label.to_string();
}
// ── Process-name-based identification ─────────────────────────────────
let by_proc: Option<&str> = match proc_stem.as_str() {
"wt" => Some("Windows Terminal"),
"windowsterminal" => Some("Windows Terminal"),
"code" => Some("VS Code"),
"code - insiders" => Some("VS Code Insiders"),
"firefox" => Some("Firefox"),
"chrome" => Some("Chrome"),
"msedge" => Some("Edge"),
"brave" => Some("Brave"),
"opera" => Some("Opera"),
"notepad" => Some("Notepad"),
"notepad++" => Some("Notepad++"),
"powershell" => Some("PowerShell"),
"pwsh" => Some("PowerShell"),
"cmd" => Some("CMD"),
"explorer" => Some("Explorer"),
"slack" => Some("Slack"),
"teams" => Some("Teams"),
"discord" => Some("Discord"),
"spotify" => Some("Spotify"),
"rider64" | "rider" => Some("Rider"),
"idea64" | "idea" => Some("IntelliJ IDEA"),
"clion64" | "clion" => Some("CLion"),
"pycharm64" | "pycharm" => Some("PyCharm"),
"webstorm64" | "webstorm" => Some("WebStorm"),
"devenv" => Some("Visual Studio"),
"sublime_text" => Some("Sublime Text"),
"atom" => Some("Atom"),
"vim" | "gvim" | "nvim-qt" => Some("Vim"),
"emacs" => Some("Emacs"),
"obsidian" => Some("Obsidian"),
"notion" => Some("Notion"),
"thunderbird" => Some("Thunderbird"),
"signal" => Some("Signal"),
"telegram" => Some("Telegram"),
_ => None,
};
if let Some(label) = by_proc {
return label.to_string();
}
// ── Generic fallback: capitalised exe stem ────────────────────────────
if proc_stem.is_empty() {
String::new()
} else {
let mut chars = proc_stem.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
}
}
/// Returns true when multiple windows of the same type are expected to
/// have completely different (user-chosen) titles, so the list should
/// show the type label prominently rather than relying on the title alone.
pub fn title_is_unreliable(&self) -> bool {
let class_lc = self.class_name.to_lowercase();
let proc_stem = exe_stem(&self.process_name);
matches!(
class_lc.as_str(),
"cascadia_hosting_window_class" // wt: tab titles are user-set
) || matches!(
proc_stem.as_str(),
"wt" | "windowsterminal" | "powershell" | "pwsh" | "cmd" | "vim" | "gvim" | "nvim-qt"
)
}
}
/// Extract the lowercase stem of a process name: "wt.exe" → "wt"
fn exe_stem(process_name: &str) -> String {
std::path::Path::new(process_name)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_lowercase()
}
// ─── Windows implementation ────────────────────────────────────────────────
#[cfg(windows)]
mod platform {
use super::WindowInfo;
use anyhow::{bail, Result};
use std::ffi::OsString;
use std::os::windows::ffi::OsStringExt;
use windows::Win32::Foundation::{BOOL, HWND, LPARAM, WPARAM};
use windows::Win32::System::Threading::{
AttachThreadInput, GetCurrentThreadId, OpenProcess, QueryFullProcessImageNameW,
PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
};
use windows::Win32::UI::WindowsAndMessaging::{
EnumWindows, GetClassNameW, GetForegroundWindow, GetWindowPlacement, GetWindowTextW,
GetWindowThreadProcessId, IsIconic, IsWindowVisible, SetWindowPos, ShowWindow, HWND_TOP,
SET_WINDOW_POS_FLAGS, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOOWNERZORDER, SWP_NOSIZE,
SWP_SHOWWINDOW, SW_MAXIMIZE, SW_SHOWNA, SW_SHOWNOACTIVATE, WINDOWPLACEMENT,
};
// Fixed 512-wchar stack buffer — avoids GetWindowTextLengthW entirely.
// GetWindowTextLengthW sends WM_GETTEXTLENGTH to every window (cross-thread
// message pump round-trip). With hundreds of windows that's the main
// source of startup lag. GetWindowTextW with a fixed buffer is non-blocking
// for windows on other threads (it uses an internal timeout-free path).
const BUF_LEN: usize = 512;
/// Retrieve the exe base-name (e.g. "wt.exe") for the given HWND.
unsafe fn get_process_name(hwnd: HWND) -> String {
let mut pid: u32 = 0;
GetWindowThreadProcessId(hwnd, Some(&mut pid));
if pid == 0 {
return String::new();
}
let handle = match OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) {
Ok(h) => h,
Err(_) => return String::new(),
};
let mut buf = [0u16; BUF_LEN];
let mut size = BUF_LEN as u32;
let ok = QueryFullProcessImageNameW(
handle,
PROCESS_NAME_WIN32,
windows::core::PWSTR(buf.as_mut_ptr()),
&mut size,
);
if ok.is_err() || size == 0 {
return String::new();
}
let path = OsString::from_wide(&buf[..size as usize])
.to_string_lossy()
.into_owned();
std::path::Path::new(&path)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string()
}
/// Retrieve the Win32 class name for the given HWND.
unsafe fn get_class_name(hwnd: HWND) -> String {
let mut buf = [0u16; 256];
let written = GetClassNameW(hwnd, &mut buf);
if written <= 0 {
return String::new();
}
OsString::from_wide(&buf[..written as usize])
.to_string_lossy()
.into_owned()
}
unsafe extern "system" fn enum_proc(hwnd: HWND, lparam: LPARAM) -> BOOL {
let vec_ptr = lparam.0 as *mut Vec<WindowInfo>;
if vec_ptr.is_null() {
return BOOL(1);
}
// Skip invisible windows fast — no message send needed
if !IsWindowVisible(hwnd).as_bool() {
return BOOL(1);
}
// Stack-allocated buffer: no heap alloc per window
let mut buf = [0u16; BUF_LEN];
let written = GetWindowTextW(hwnd, &mut buf);
if written <= 0 {
return BOOL(1);
}
let title = OsString::from_wide(&buf[..written as usize])
.to_string_lossy()
.into_owned();
if !title.is_empty() {
let process_name = get_process_name(hwnd);
let class_name = get_class_name(hwnd);
(*vec_ptr).push(WindowInfo {
hwnd: hwnd.0 as usize,
title,
process_name,
class_name,
});
}
BOOL(1)
}
pub fn enumerate_windows() -> Result<Vec<WindowInfo>> {
let mut windows: Vec<WindowInfo> = Vec::with_capacity(128);
let ptr = &mut windows as *mut Vec<WindowInfo>;
unsafe {
EnumWindows(Some(enum_proc), LPARAM(ptr as isize))?;
}
Ok(windows)
}
/// Bring a window to the foreground **without changing its show-state**.
///
/// Algorithm (mirrors the Python reference implementation):
///
/// 1. Snapshot `WINDOWPLACEMENT` so we know the current `showCmd`.
/// 2. Attach our input queue to the foreground thread (and the target
/// thread) so the OS foreground lock cannot block us.
/// 3. If the window is iconic (minimised) → `SW_SHOWNOACTIVATE` (4).
/// Otherwise → `SW_SHOWNA` (8) — make it visible without stealing focus.
/// 4. `SetWindowPos(..., HWND_TOP, SWP_NOSIZE|SWP_NOMOVE|SWP_NOACTIVATE|...)`
/// raises the Z-order without moving/resizing/activating the window.
/// 5. Detach input queues (always, even on error).
/// 6. If the window was **not** maximised, restore the original placement so
/// position/size are exactly as before. If it **was** maximised, skip
/// `SetWindowPlacement` — calling it on a maximised window forces it back
/// to restored size, which is the bug we are fixing.
pub fn bring_to_front(info: &WindowInfo) -> Result<()> {
let hwnd = HWND(info.hwnd as isize);
unsafe {
// ── 1. Snapshot current placement ────────────────────────────────
let mut wp = WINDOWPLACEMENT {
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
..Default::default()
};
if GetWindowPlacement(hwnd, &mut wp).is_err() {
bail!("GetWindowPlacement failed for '{}'", info.title);
}
let original_show_cmd = wp.showCmd;
// ── 2. Attach input queues ────────────────────────────────────────
let our_tid = GetCurrentThreadId();
let fg_hwnd = GetForegroundWindow();
let fg_tid = GetWindowThreadProcessId(fg_hwnd, None);
let target_tid = GetWindowThreadProcessId(hwnd, None);
let attached_fg = fg_tid != 0
&& fg_tid != our_tid
&& AttachThreadInput(our_tid, fg_tid, true).as_bool();
let attached_target = target_tid != 0
&& target_tid != our_tid
&& target_tid != fg_tid
&& AttachThreadInput(our_tid, target_tid, true).as_bool();
// ── 3 & 4. Show without activating, then raise Z-order ───────────
let show_result = (|| -> Result<()> {
if IsIconic(hwnd).as_bool() {
// Minimised → show without activating
ShowWindow(hwnd, SW_SHOWNOACTIVATE);
} else {
// Normal or Maximised → ensure visible without focus
ShowWindow(hwnd, SW_SHOWNA);
}
SetWindowPos(
hwnd,
HWND_TOP,
0,
0,
0,
0,
SET_WINDOW_POS_FLAGS(
SWP_NOSIZE.0
| SWP_NOMOVE.0
| SWP_NOACTIVATE.0
| SWP_NOOWNERZORDER.0
| SWP_SHOWWINDOW.0,
),
)?;
Ok(())
})();
// ── 5. Always detach ──────────────────────────────────────────────
if attached_fg {
let _ = AttachThreadInput(our_tid, fg_tid, false);
}
if attached_target {
let _ = AttachThreadInput(our_tid, target_tid, false);
}
show_result?;
// ── 6. Restore placement only when NOT maximised ──────────────────
// Calling SetWindowPlacement on a maximised window forces it to
// restored size — exactly the bug reported. Skip it in that case.
if original_show_cmd != SW_MAXIMIZE.0 as u32 {
// best-effort; ignore errors (window may have moved legitimately)
let _ = windows::Win32::UI::WindowsAndMessaging::SetWindowPlacement(hwnd, &wp);
}
}
Ok(())
}
pub fn close_window(info: &WindowInfo) -> Result<()> {
use windows::Win32::UI::WindowsAndMessaging::{PostMessageW, WM_CLOSE};
let hwnd = HWND(info.hwnd as isize);
unsafe {
// PostMessageW is fire-and-forget — never blocks
PostMessageW(hwnd, WM_CLOSE, WPARAM(0), LPARAM(0))?;
}
Ok(())
}
}
// ─── Non-Windows stub ──────────────────────────────────────────────────────
#[cfg(not(windows))]
mod platform {
use super::WindowInfo;
use anyhow::{bail, Result};
pub fn enumerate_windows() -> Result<Vec<WindowInfo>> {
// On non-Windows, return a synthetic list so tests can exercise
// the search / coloring logic without a real display.
Ok(vec![
WindowInfo {
hwnd: 1,
title: "Firefox — GitHub".into(),
process_name: "firefox.exe".into(),
class_name: "MozillaWindowClass".into(),
},
WindowInfo {
hwnd: 2,
title: "Visual Studio Code".into(),
process_name: "Code.exe".into(),
class_name: "Chrome_WidgetWin_1".into(),
},
WindowInfo {
hwnd: 3,
title: "my-project".into(),
process_name: "wt.exe".into(),
class_name: "CASCADIA_HOSTING_WINDOW_CLASS".into(),
},
WindowInfo {
hwnd: 4,
title: "ssh prod-server".into(),
process_name: "wt.exe".into(),
class_name: "CASCADIA_HOSTING_WINDOW_CLASS".into(),
},
WindowInfo {
hwnd: 5,
title: "Notepad — readme.txt".into(),
process_name: "notepad.exe".into(),
class_name: "Notepad".into(),
},
WindowInfo {
hwnd: 6,
title: "Task Manager".into(),
process_name: "Taskmgr.exe".into(),
class_name: "TaskManagerWindow".into(),
},
])
}
pub fn bring_to_front(info: &WindowInfo) -> Result<()> {
bail!(
"bring_to_front is not supported on this platform (hwnd={})",
info.hwnd
);
}
pub fn close_window(info: &WindowInfo) -> Result<()> {
bail!(
"close_window is not supported on this platform (hwnd={})",
info.hwnd
);
}
}
// ─── Public re-exports ─────────────────────────────────────────────────────
pub use platform::{bring_to_front, close_window, enumerate_windows};