1pub 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
21pub 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 let mut title = "E Window".to_string();
35 let mut appname = String::new();
36 let mut width = 0u32; let mut height = 0u32; 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 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 let mut parent_pid: Option<u32> = None;
53
54 let mut decode_debug = false;
56 let mut _restore_storage = false;
58 let mut _has_been_specified = false;
59 let mut box_type = MessageBoxType::Ok;
61 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 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 }
199 }
200 }
201
202 eprintln!("[DEBUG] Final MessageBoxType: {:?}", box_type);
203
204 eprintln!("[DEBUG] After CLI parsing: title='{}', size={}x{}, pos={}x{}",
206 title, width, height, x, y);
207
208 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 use std::sync::mpsc;
218 let (tx, rx) = mpsc::channel();
219
220 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 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 (first.clone(), false)
237 }
238 } else {
239 use std::sync::{Arc, Mutex};
241 let initial_buffer = Arc::new(Mutex::new(Vec::new()));
242 let initial_buffer_clone = initial_buffer.clone();
243 control::start_stdin_listener_with_buffer(tx.clone(), initial_buffer_clone);
245 let mut waited = 0;
247 let max_wait = 200; 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 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 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; app::default_card_with_hwnd(hwnd)
286 } else if input_data.trim().is_empty() && !_text.is_empty() {
287 eprintln!("[DEBUG] Creating message box content: type={:?}, body='{}'", box_type, _text);
289
290 let mut content = format!("type | {:?} | string\n", box_type);
291
292 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 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 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 println!("Warning: Unknown argument: {:?}", arg);
384 }
385 }
386 }
387 }
388 actual_input = input_lines.collect::<Vec<_>>().join("\n");
390 }
391
392 eprintln!("[DEBUG] After content parsing: title='{}', size={}x{}, pos={}x{}",
394 title, width, height, x, y);
395
396 if !received_explicit_position_args {
398 eprintln!("[DEBUG] Auto-centering: received_explicit_position_args=false, will auto-center after layout");
399 x = -1; y = -1; 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 if let Some(pool_size) = w_pool_cnt {
410 if let (Some(pid), Some(_ndx)) = (parent_pid, w_pool_ndx) {
412 let parent_alive = Arc::new(std::sync::Mutex::new(true));
414 let parent_alive_clone = parent_alive.clone();
415 std::thread::spawn(move || {
416 std::thread::sleep(Duration::from_millis(100));
418 while *parent_alive_clone.lock().unwrap() {
419 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 std::process::exit(0);
429 }
430 std::thread::sleep(Duration::from_millis(500));
432 }
433 });
434 }
435
436 if w_pool_ndx.is_none() && !args.iter().any(|a| a == "--w-pool-manager") {
438 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 while let Some(idx) = child_args.iter().position(|a| a == "--w-pool-ndx") {
445 child_args.drain(idx..=idx + 1);
446 }
447 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 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 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(()); }
474 }
475
476 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 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 let exe = std::env::current_exe().expect("Failed to get current exe");
492 let mut child_args = args.clone();
493 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 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 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 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 let Some(ndx) = w_pool_ndx {
564 title = format!("{} (Window #{})", title, ndx);
565 }
566
567 let mut viewport_builder = egui::ViewportBuilder::default().with_title(&title);
569
570 if !_text.is_empty() {
572 viewport_builder = viewport_builder
575 .with_inner_size([250.0, 100.0]) .with_position(egui::pos2(f32::INFINITY, f32::INFINITY)); } else {
578 if width > 0 && height > 0 {
580 viewport_builder = viewport_builder.with_inner_size([width as f32, height as f32]);
581 }
582
583 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, ..Default::default()
593 };
594 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; 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), )
629 .with_input_data_and_mode(actual_input, editor_mode);
630 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#[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 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 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 for (pid, process) in sys.processes() {
721 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}