e_window/
lib.rs

1//! Library interface for launching the e_window app with custom arguments.
2
3// Re-export shared types from e_window_types for convenience
4pub use e_window_types::{MessageBoxType, MessageBoxIcon, MessageBoxResult};
5
6pub mod app;
7pub mod control;
8pub mod parser;
9pub mod pool_manager;
10pub mod position_grid;
11pub mod position_grid_manager;
12pub mod uxn;
13
14use getargs::{Arg, Options};
15use std::env::current_exe;
16use std::fs;
17use std::process::Command;
18use std::sync::Arc;
19use std::time::{Duration, Instant};
20
21/// Run the e_window app with the given arguments (excluding program name).
22pub fn run_window<I, S>(args: I) -> eframe::Result<()>
23where
24    I: IntoIterator<Item = S>,
25    S: AsRef<str>,
26{
27    let args = args
28        .into_iter()
29        .map(|s| s.as_ref().to_string())
30        .collect::<Vec<_>>();
31    let mut opts = Options::new(args.iter().map(String::as_str));
32
33    // Defaults - use auto-centering for position
34    let mut title = "E Window".to_string();
35    let mut appname = String::new();
36    let mut width = 0u32;  // Will be set by content or app defaults
37    let mut height = 0u32; // Will be set by content or app defaults
38    
39    // Start with no explicit position - will be auto-centered unless CLI args specify otherwise
40    let mut x = 0i32;
41    let mut y = 0i32;
42    let mut input_file: Option<String> = None;
43    let mut follow_hwnd: Option<usize> = None;
44    let mut positional_args = Vec::new();
45
46    // New pool options
47    let mut w_pool_cnt: Option<usize> = None;
48    let mut w_pool_ndx: Option<usize> = None;
49    let mut w_pool_rate: Option<u64> = None;
50
51    // Parent PID for child windows
52    let mut parent_pid: Option<u32> = None;
53
54    // Decode debug flag
55    let mut decode_debug = false;
56    // Storage restoration flag
57    let mut _restore_storage = false;
58    let mut _has_been_specified = false;
59    // Add a variable to store the MessageBoxType
60    let mut box_type = MessageBoxType::Ok;
61    // Variable to store text argument
62    let mut _text = String::new();
63
64    while let Some(arg) = opts.next_arg().expect("argument parsing error") {
65        match arg {
66            Arg::Long("title") => {
67                if let Ok(val) = opts.value() {
68                    title = val.to_string();
69                    _has_been_specified = true;
70                    eprintln!("[DEBUG] Processed --title '{}', has_been_specified set to true", val);
71                }
72            }
73            Arg::Long("width") => {
74                if let Ok(val) = opts.value() {
75                    width = val.parse().unwrap_or(width);
76                    _has_been_specified = true;
77                }
78            }
79            Arg::Long("height") => {
80                if let Ok(val) = opts.value() {
81                    height = val.parse().unwrap_or(height);
82                    _has_been_specified = true;
83                }
84            }
85            Arg::Long("x") => {
86                if let Ok(val) = opts.value() {
87                    x = val.parse().unwrap_or(x);
88                    _has_been_specified = true;
89                }
90            }
91            Arg::Long("y") => {
92                if let Ok(val) = opts.value() {
93                    y = val.parse().unwrap_or(y);
94                    _has_been_specified = true;
95                }
96            }
97            Arg::Long("appname") => {
98                if let Ok(val) = opts.value() {
99                    appname = val.to_string();
100                    _has_been_specified = true;
101                }
102            }
103            Arg::Short('i') | Arg::Long("input-file") => {
104                if let Ok(val) = opts.value() {
105                    input_file = Some(val.to_string());
106                    _has_been_specified = true;
107                }
108            }
109            Arg::Long("follow-hwnd") => {
110                if let Ok(val) = opts.value() {
111                    // Accept both decimal and hex (with 0x prefix)
112                    follow_hwnd = if let Some(stripped) = val.strip_prefix("0x") {
113                        usize::from_str_radix(stripped, 16).ok()
114                    } else {
115                        val.parse().ok()
116                    };
117                    _has_been_specified = true;
118                }
119            }
120            Arg::Long("w-pool-cnt") => {
121                if let Ok(val) = opts.value() {
122                    w_pool_cnt = val.parse().ok();
123                    _has_been_specified = true;
124                }
125            }
126            Arg::Long("w-pool-ndx") => {
127                if let Ok(val) = opts.value() {
128                    w_pool_ndx = val.parse().ok();
129                    _has_been_specified = true;
130                }
131            }
132            Arg::Long("w-pool-rate") => {
133                if let Ok(val) = opts.value() {
134                    w_pool_rate = val.parse().ok();
135                    _has_been_specified = true;
136                }
137            }
138            Arg::Long("parent-pid") => {
139                if let Ok(val) = opts.value() {
140                    parent_pid = val.parse().ok();
141                    _has_been_specified = true;
142                }
143            }
144            Arg::Long("decode-debug") => {
145                decode_debug = true;
146                _has_been_specified = true;
147            }
148            Arg::Long("restore-storage") => {
149                _restore_storage = true;
150                _has_been_specified = true;
151            }
152            Arg::Long("type") => {
153                if let Ok(val) = opts.value() {
154                    box_type = MessageBoxType::from_str(&val).unwrap_or(MessageBoxType::Ok);
155                    eprintln!("[DEBUG] Parsed --type: {:?}", box_type);
156                }
157            }
158            Arg::Long("body") => {
159                if let Ok(val) = opts.value() {
160                    _text = val.to_string();
161                    _has_been_specified = true;
162                    eprintln!("[DEBUG] Processed --body '{}', has_been_specified set to true", val);
163                }
164            }
165            Arg::Short('h') | Arg::Long("help") => {
166                eprintln!(
167                    r#"Usage: e_window [OPTIONS] [FILES...]
168    --appname <NAME>     Set app name (default: executable name)
169    --title <TITLE>      Set window title (default: "E Window")
170    --width <WIDTH>      Set window width (default: content-sized)
171    --height <HEIGHT>    Set window height (default: content-sized)
172    --x <X>              Set window X position (default: auto-centered)
173    --y <Y>              Set window Y position (default: auto-centered)
174    -i, --input-file <FILE>  Read input data from file
175    --follow-hwnd <HWND> Follow HWND (default: None)
176    --w-pool-cnt <N>     Keep at least N windows open at all times
177    --w-pool-ndx <N>     (internal) Index of this window instance
178    --w-pool-rate <MS>   Minimum milliseconds between opening new windows (default: 1000)
179    --type <TYPE>        Message box type (Ok, OkCancel, YesNo, YesNoDefNo, YesNoCancel, YesNoCancelDefNo, RetryCancel, TextInput, FileSelection)
180    --body <TEXT>        Message body text
181    --decode-debug       Enable debug decoding mode
182    -h, --help           Show this help and exit
183    --version            Show version and exit
184Any other positional arguments are collected as files or piped input."#
185                );
186                return Ok(());
187            }
188            Arg::Long("version") => {
189                println!("e_window {}", env!("CARGO_PKG_VERSION"));
190                println!("Built on {}", env!("BUILD_TIMESTAMP"));
191                return Ok(());
192            }
193            Arg::Positional(val) => {
194                positional_args.push(val.to_string());
195            }
196            Arg::Short(_) | Arg::Long(_) => {
197                // Ignore unknown flags for now
198            }
199        }
200    }
201
202    eprintln!("[DEBUG] Final MessageBoxType: {:?}", box_type);
203
204    // Debug: Print the parsed window config from command line arguments
205    eprintln!("[DEBUG] After CLI parsing: title='{}', size={}x{}, pos={}x{}", 
206             title, width, height, x, y);
207
208    // Default appname to executable name (without extension) if not set
209    if appname.is_empty() {
210        appname = current_exe()
211            .ok()
212            .and_then(|p| p.file_stem().map(|s| s.to_string_lossy().to_string()))
213            .unwrap_or_else(|| "e_window".to_string());
214    }
215
216    // Set up control channel once, share between stdin thread and app
217    use std::sync::mpsc;
218    let (tx, rx) = mpsc::channel();
219
220    // Read input data: from file if specified, else from positional args, else empty
221    let (input_data, mut editor_mode) = if let Some(file) = input_file {
222        (
223            fs::read_to_string(file).unwrap_or_else(|_| "".to_string()),
224            false,
225        )
226    } else if !positional_args.is_empty() {
227        // If the first positional argument looks like a file and exists, use it as a file
228        let first = &positional_args[0];
229        if fs::metadata(first).is_ok() {
230            (
231                fs::read_to_string(first).unwrap_or_else(|_| "".to_string()),
232                false,
233            )
234        } else {
235            // Otherwise, treat it as the card content
236            (first.clone(), false)
237        }
238    } else {
239        // Buffer initial lines from stdin asynchronously and use them as input_data
240        use std::sync::{Arc, Mutex};
241        let initial_buffer = Arc::new(Mutex::new(Vec::new()));
242        let initial_buffer_clone = initial_buffer.clone();
243        // Start the stdin listener, but also buffer the first lines
244        control::start_stdin_listener_with_buffer(tx.clone(), initial_buffer_clone);
245        // Wait briefly for initial input (event-driven, but with a short timeout)
246        let mut waited = 0;
247        let max_wait = 200; // ms
248        while waited < max_wait {
249            if !initial_buffer.lock().unwrap().is_empty() {
250                break;
251            }
252            std::thread::sleep(std::time::Duration::from_millis(10));
253            waited += 10;
254        }
255        let input_data = initial_buffer.lock().unwrap().join("\n");
256        (input_data, false)
257    };
258
259    // If input_data is empty, use your DEFAULT_CARD only if no meaningful CLI args were provided
260    eprintln!("[DEBUG] input_data content: '{}'", input_data.replace('\n', "\\n"));
261    eprintln!("[DEBUG] input_data.trim().is_empty(): {}", input_data.trim().is_empty());
262    
263    // Check if we received meaningful CLI arguments (indicating API call)
264    // This needs to be calculated before any content parsing that might change values
265    let received_explicit_cli_args = _has_been_specified;
266    let received_explicit_position_args = x != 0 || y != 0;
267    eprintln!("[DEBUG] received_explicit_cli_args: {}", received_explicit_cli_args);
268    eprintln!("[DEBUG] received_explicit_position_args: {}", received_explicit_position_args);
269    
270    let input_data = if input_data.trim().is_empty() && !received_explicit_cli_args {
271        eprintln!("[DEBUG] Using default card: input_data.trim().is_empty()={}, !received_explicit_cli_args={}", 
272                 input_data.trim().is_empty(), !received_explicit_cli_args);
273        println!("Warning: No input data provided, using default card template.");
274        let hwnd = {
275            #[cfg(target_os = "windows")]
276            {
277                unsafe { winapi::um::winuser::GetForegroundWindow() as usize }
278            }
279            #[cfg(not(target_os = "windows"))]
280            {
281                0
282            }
283        };
284        editor_mode = true; // Set editor mode if using default card
285        app::default_card_with_hwnd(hwnd)
286    } else if input_data.trim().is_empty() && !_text.is_empty() {
287        // We have message box body text, create clean message box content
288        eprintln!("[DEBUG] Creating message box content: type={:?}, body='{}'", box_type, _text);
289        
290        let mut content = format!("type | {:?} | string\n", box_type);
291        
292        // Add input_type for input-based message boxes
293        match box_type {
294            MessageBoxType::TextInput => {
295                content.push_str("input_type | text | string\n");
296            },
297            MessageBoxType::FileSelection => {
298                content.push_str("input_type | file | string\n");
299            },
300            _ => {}
301        }
302        
303        content.push_str(&format!("\n{}", _text));
304        content
305    } else {
306        eprintln!("[DEBUG] NOT using default card: input_data.trim().is_empty()={}, !received_explicit_cli_args={}", 
307                 input_data.trim().is_empty(), !received_explicit_cli_args);
308        input_data
309    };
310
311    // Parse first line for CLI args, and use the rest as input_data
312    eprintln!("[DEBUG] Before content parsing: title='{}', size={}x{}, pos={}x{}", 
313             title, width, height, x, y);
314    let mut input_lines = input_data.lines();
315    let mut actual_input = String::new();
316    if let Some(first_line) = input_lines.next() {
317        let input_args = shell_words::split(first_line).unwrap_or_default();
318        if !input_args.is_empty() {
319            let mut opts = Options::new(input_args.iter().map(String::as_str));
320            while let Some(arg) = opts.next_arg().expect("argument parsing error") {
321                match arg {
322                    Arg::Long("follow-hwnd") => {
323                        if let Ok(val) = opts.value() {
324                            // Accept both decimal and hex (with 0x prefix)
325                            follow_hwnd = if let Some(stripped) = val.strip_prefix("0x") {
326                                usize::from_str_radix(stripped, 16).ok()
327                            } else {
328                                val.parse().ok()
329                            };
330                        }
331                    }
332                    Arg::Long("title") => {
333                        if let Ok(val) = opts.value() {
334                            title = val.to_string();
335                        }
336                    }
337                    Arg::Long("width") => {
338                        if let Ok(val) = opts.value() {
339                            width = val.parse().unwrap_or(width);
340                        }
341                    }
342                    Arg::Long("height") => {
343                        if let Ok(val) = opts.value() {
344                            height = val.parse().unwrap_or(height);
345                        }
346                    }
347                    Arg::Long("x") => {
348                        if let Ok(val) = opts.value() {
349                            x = val.parse().unwrap_or(x);
350                        }
351                    }
352                    Arg::Long("y") => {
353                        if let Ok(val) = opts.value() {
354                            y = val.parse().unwrap_or(y);
355                        }
356                    }
357                    Arg::Long("appname") => {
358                        if let Ok(val) = opts.value() {
359                            appname = val.to_string();
360                        }
361                    }
362                    Arg::Long("decode-debug") => {
363                        println!(
364                            "Warning: --decode-debug is deprecated, use --decode-debug instead."
365                        );
366                        decode_debug = true;
367                        _has_been_specified = true;
368                    }
369                    Arg::Long("type") => {
370                        if let Ok(val) = opts.value() {
371                            box_type = MessageBoxType::from_str(&val).unwrap_or(MessageBoxType::Ok);
372                            eprintln!("[DEBUG] Parsed --type from first line: {:?}", box_type);
373                        }
374                    }
375                    Arg::Long("body") => {
376                        if let Ok(val) = opts.value() {
377                            _text = val.to_string();
378                            eprintln!("[DEBUG] Parsed --body from first line: '{}'", val);
379                        }
380                    }
381                    _ => {
382                        // Ignore other flags for now
383                        println!("Warning: Unknown argument: {:?}", arg);
384                    }
385                }
386            }
387        }
388        // Use the rest of the lines as the actual input
389        actual_input = input_lines.collect::<Vec<_>>().join("\n");
390    }
391
392    // Debug: Print the window config after content parsing (where the problem occurs)
393    eprintln!("[DEBUG] After content parsing: title='{}', size={}x{}, pos={}x{}", 
394             title, width, height, x, y);
395
396    // If no explicit position args were provided, let the app handle auto-centering after layout
397    if !received_explicit_position_args {
398        eprintln!("[DEBUG] Auto-centering: received_explicit_position_args=false, will auto-center after layout");
399        // Set a special flag value to indicate auto-centering is desired
400        // The app will detect this and center the window after determining content size
401        x = -1;  // Special flag value for auto-center-x
402        y = -1;  // Special flag value for auto-center-y
403        eprintln!("[DEBUG] Set auto-center flags: pos=({}, {})", x, y);
404    } else {
405        eprintln!("[DEBUG] Auto-centering: received_explicit_position_args=true, skipping centering");
406    }
407
408    // --- Window pool logic ---
409    if let Some(pool_size) = w_pool_cnt {
410        // Monitor parent PID if this is a pool child
411        if let (Some(pid), Some(_ndx)) = (parent_pid, w_pool_ndx) {
412            // In a loop, check if the parent PID is still running
413            let parent_alive = Arc::new(std::sync::Mutex::new(true));
414            let parent_alive_clone = parent_alive.clone();
415            std::thread::spawn(move || {
416                // Briefly wait for the parent PID to be set
417                std::thread::sleep(Duration::from_millis(100));
418                while *parent_alive_clone.lock().unwrap() {
419                    // Check if the parent process is still running
420                    let is_running = {
421                        let mut sys = sysinfo::System::new_all();
422                        sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
423                        sys.process(sysinfo::Pid::from(pid as usize)).is_some()
424                    };
425                    if !is_running {
426                        eprintln!("[DEBUG] Detected parent process (PID {}) has exited.", pid);
427                        // If the parent has exited, terminate this child window
428                        std::process::exit(0);
429                    }
430                    // Sleep briefly before checking again
431                    std::thread::sleep(Duration::from_millis(500));
432                }
433            });
434        }
435
436        // Only spawn the pool manager if this is NOT a child window and NOT already the pool manager
437        if w_pool_ndx.is_none() && !args.iter().any(|a| a == "--w-pool-manager") {
438            // Remove --w-pool-cnt and its value from args for child windows
439            let mut child_args = args.clone();
440            if let Some(idx) = child_args.iter().position(|a| a == "--w-pool-cnt") {
441                child_args.drain(idx..=idx + 1);
442            }
443            // Remove any --w-pool-ndx from args (we'll add it per child)
444            while let Some(idx) = child_args.iter().position(|a| a == "--w-pool-ndx") {
445                child_args.drain(idx..=idx + 1);
446            }
447            // Remove --w-pool-rate and its value from args for child windows
448            if let Some(idx) = child_args.iter().position(|a| a == "--w-pool-rate") {
449                child_args.drain(idx..=idx + 1);
450            }
451            let exe = std::env::current_exe().expect("Failed to get current exe");
452            let rate_ms = w_pool_rate.unwrap_or(1000);
453
454            // Spawn the pool manager as a detached process and exit this process
455            let mut cmd = std::process::Command::new(&exe);
456            cmd.arg("--w-pool-manager")
457                .arg("--parent-pid")
458                .arg(std::process::id().to_string())
459                .arg(format!("--w-pool-cnt={}", pool_size))
460                .arg(format!("--w-pool-rate={}", rate_ms))
461                .args(&child_args);
462
463            let mut manager_process = cmd.spawn().expect("Failed to spawn pool manager");
464            println!("e_window: Pool manager started...");
465
466            // Wait for the pool manager to exit
467            let status = manager_process
468                .wait()
469                .expect("Failed to wait on pool manager");
470            println!("e_window: Pool manager exited with status: {}", status);
471
472            return Ok(()); // Exit the original process
473        }
474    }
475
476    // Pool manager logic (runs in a separate process)
477    if args.iter().any(|a| a == "--w-pool-manager") {
478        let pool_size = w_pool_cnt.unwrap_or(1);
479        let rate_ms = w_pool_rate.unwrap_or(1000);
480
481        // Spawn GUI for the pool manager
482        let options = eframe::NativeOptions {
483            viewport: egui::ViewportBuilder::default()
484                .with_inner_size([400.0, 200.0])
485                .with_title("e_window Pool Manager")
486                .with_always_on_top(),
487            ..Default::default()
488        };
489
490        // Spawn windows in a background thread as before
491        let exe = std::env::current_exe().expect("Failed to get current exe");
492        let mut child_args = args.clone();
493        // Remove pool manager args as before...
494        if let Some(idx) = child_args.iter().position(|a| a == "--w-pool-manager") {
495            child_args.remove(idx);
496        }
497        if let Some(idx) = child_args.iter().position(|a| a == "--w-pool-cnt") {
498            child_args.drain(idx..=idx + 1);
499        }
500        while let Some(idx) = child_args.iter().position(|a| a == "--w-pool-ndx") {
501            child_args.drain(idx..=idx + 1);
502        }
503        if let Some(idx) = child_args.iter().position(|a| a == "--w-pool-rate") {
504            child_args.drain(idx..=idx + 1);
505        }
506
507        let pool_manager = pool_manager::PoolManagerApp::new(pool_size, rate_ms);
508        let pool_manager_thread = Arc::new(pool_manager);
509
510        // Clone for thread
511        let pool_manager_thread_clone = Arc::clone(&pool_manager_thread);
512
513        std::thread::spawn(move || {
514            let mut next_index = 1;
515            loop {
516                if pool_manager_thread_clone
517                    .shutdown
518                    .load(std::sync::atomic::Ordering::Relaxed)
519                {
520                    break;
521                }
522                let count = count_running_windows(&exe);
523                if count < pool_size {
524                    let to_spawn = pool_size - count;
525                    for _ in 0..to_spawn {
526                        let mut args_with_index = child_args.clone();
527                        args_with_index.push("--w-pool-ndx".to_string());
528                        args_with_index.push(next_index.to_string());
529                        args_with_index.push("--parent-pid".to_string());
530                        args_with_index.push(std::process::id().to_string());
531                        println!("Spawning: {:?} {:?}", exe, args_with_index);
532                        // When you spawn a child:
533                        if let Ok(child) = Command::new(&exe).args(&args_with_index).spawn() {
534                            *pool_manager_thread_clone.spawned.lock().unwrap() += 1;
535                            *pool_manager_thread_clone.last_spawn.lock().unwrap() = Instant::now();
536                            pool_manager_thread_clone
537                                .children
538                                .lock()
539                                .unwrap()
540                                .push(child);
541                        }
542                        next_index += 1;
543                        std::thread::sleep(Duration::from_millis(rate_ms));
544                    }
545                }
546                std::thread::sleep(Duration::from_millis(rate_ms));
547            }
548        });
549
550        // Run the pool manager GUI
551        return eframe::run_native(
552            "e_window Pool Manager",
553            options,
554            Box::new(move |_cc| {
555                Ok::<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>>(Box::new(
556                    (*pool_manager_thread).clone(),
557                ))
558            }),
559        );
560    }
561
562    // If you want to use the index in your window title:
563    if let Some(ndx) = w_pool_ndx {
564        title = format!("{} (Window #{})", title, ndx);
565    }
566
567    // Launch the GUI immediately, passing the shared receiver
568    let mut viewport_builder = egui::ViewportBuilder::default().with_title(&title);
569    
570    // For message boxes, calculate sizes dynamically based on font metrics
571    if !_text.is_empty() {
572        // We need to defer sizing to the first frame when egui context is available
573        // For now, use minimal sizing and let the app adjust after font metrics are available
574        viewport_builder = viewport_builder
575            .with_inner_size([250.0, 100.0]) // Minimal initial size
576            .with_position(egui::pos2(f32::INFINITY, f32::INFINITY)); // This centers the window
577    } else {
578        // Only set size if explicitly provided (not 0)
579        if width > 0 && height > 0 {
580            viewport_builder = viewport_builder.with_inner_size([width as f32, height as f32]);
581        }
582        
583        // Only set position if explicitly provided (not -1 for auto-center)  
584        if x != -1 && y != -1 {
585            viewport_builder = viewport_builder.with_position([x as f32, y as f32]);
586        }
587    }
588    
589    let options = eframe::NativeOptions {
590        viewport: viewport_builder,
591        persist_window: false, // Don't restore previous window state for message boxes
592        ..Default::default()
593    };
594    // If actual_input is empty, use your DEFAULT_CARD
595    let actual_input = if actual_input.trim().is_empty() {
596        println!("Warning: No input data provided, using default card template.");
597        let hwnd = {
598            #[cfg(target_os = "windows")]
599            {
600                unsafe { winapi::um::winuser::GetForegroundWindow() as usize }
601            }
602            #[cfg(not(target_os = "windows"))]
603            {
604                0
605            }
606        };
607        editor_mode = true; // Set editor mode if using default card
608        app::default_card_with_hwnd(hwnd)
609    } else {
610        actual_input
611    };
612    eframe::run_native(
613        &appname,
614        options,
615        Box::new(move |cc| {
616            eprintln!("[DEBUG] Creating app with values: width={}, height={}, x={}, y={}, title='{}'", 
617                     width, height, x, y, title);
618            let app = app::App::with_initial_window(
619                width as f32,
620                height as f32,
621                x as f32,
622                y as f32,
623                title.clone(),
624                cc.storage,
625                follow_hwnd,
626                decode_debug,
627                format!("{:?}", box_type), // Convert MessageBoxType to String
628            )
629            .with_input_data_and_mode(actual_input, editor_mode);
630            // Pass the receiver to the app
631            Ok::<Box<dyn eframe::App>, Box<dyn std::error::Error + Send + Sync>>(Box::new(
632                app.with_control_receiver(rx),
633            ))
634        }),
635    )
636}
637
638// Helper: count running windows (processes) with our exe name
639#[cfg(target_os = "windows")]
640fn count_running_windows(_exe: &std::path::Path) -> usize {
641    use std::ffi::OsString;
642
643    use std::os::windows::ffi::OsStringExt;
644    use sysinfo::System;
645    use winapi::um::winuser::{
646        EnumWindows, GetWindowTextW, GetWindowThreadProcessId, IsWindowVisible,
647    };
648
649    // Data struct to pass to callback
650    struct EnumData<'a> {
651        our_pids: &'a [u32],
652        count: usize,
653    }
654
655    unsafe extern "system" fn enum_windows_proc(
656        hwnd: winapi::shared::windef::HWND,
657        lparam: winapi::shared::minwindef::LPARAM,
658    ) -> i32 {
659        let data = &mut *(lparam as *mut EnumData);
660        let mut pid = 0u32;
661        if IsWindowVisible(hwnd) == 0 {
662            return 1;
663        }
664        GetWindowThreadProcessId(hwnd, &mut pid);
665        if !data.our_pids.contains(&pid) {
666            return 1;
667        }
668        let mut buf = [0u16; 256];
669        let len = GetWindowTextW(hwnd, buf.as_mut_ptr(), buf.len() as i32);
670        if len > 0 {
671            let title = OsString::from_wide(&buf[..len as usize])
672                .to_string_lossy()
673                .to_string();
674            if title.contains("Window #") {
675                data.count += 1;
676            }
677        }
678        1
679    }
680
681    let mut sys = System::new_all();
682    sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
683
684    // Collect all process IDs for our exe
685    let mut our_pids = Vec::new();
686    for (pid, process) in sys.processes() {
687        let name = process.name().to_ascii_lowercase();
688        if name == "e_window.exe" || name == "e_window" {
689            our_pids.push(pid.as_u32());
690        }
691    }
692
693    let mut data = EnumData {
694        our_pids: &our_pids,
695        count: 0,
696    };
697
698    unsafe {
699        EnumWindows(
700            Some(enum_windows_proc),
701            &mut data as *mut _ as winapi::shared::minwindef::LPARAM,
702        );
703    }
704    data.count
705}
706
707#[cfg(not(target_os = "windows"))]
708fn count_running_windows(_exe: &std::path::Path) -> usize {
709    #[cfg(not(target_os = "windows"))]
710    use sysinfo::System;
711    #[cfg(target_os = "windows")]
712    use sysinfo::{ProcessExt, System, SystemExt};
713    let mut sys = System::new_all();
714    #[cfg(target_os = "windows")]
715    sys.refresh_processes();
716    #[cfg(not(target_os = "windows"))]
717    sys.refresh_processes(sysinfo::ProcessesToUpdate::All, true);
718    let mut our_pids = Vec::new();
719    // Collect all process IDs for our exe that are pool children
720    for (pid, process) in sys.processes() {
721        // Match exe name (case-insensitive) and check for --w-pool-ndx in cmdline
722        let is_pool_child = process.cmd().iter().any(|arg| arg == "--w-pool-ndx");
723        let exe_name = process.name().to_ascii_lowercase();
724        if is_pool_child && (exe_name == "e_window.exe" || exe_name == "e_window") {
725            our_pids.push(pid.as_u32());
726        }
727    }
728
729    our_pids.len()
730}