1use std::cell::RefCell;
2use std::collections::{BTreeMap, VecDeque};
3use std::rc::{Rc, Weak};
4
5use thiserror::Error;
6
7use crate::ScriptCallback;
8
9const MAX_WINDOWS: usize = 16;
10const MAX_PENDING_COMMANDS: usize = 64;
11
12#[derive(Clone, Debug, PartialEq)]
13pub struct ScriptWindowSpec {
14 pub id: String,
15 pub title: String,
16 pub width: f64,
17 pub height: f64,
18 pub focus: bool,
19}
20
21impl ScriptWindowSpec {
22 pub fn validate(&self) -> Result<(), WindowCommandError> {
28 if !valid_window_id(&self.id) {
29 return Err(WindowCommandError::InvalidId(self.id.clone()));
30 }
31 if self.title.trim().is_empty() || self.title.chars().count() > 256 {
32 return Err(WindowCommandError::InvalidTitle);
33 }
34 if !valid_dimension(self.width) || !valid_dimension(self.height) {
35 return Err(WindowCommandError::InvalidBounds {
36 width: self.width,
37 height: self.height,
38 });
39 }
40 Ok(())
41 }
42}
43
44fn valid_window_id(id: &str) -> bool {
45 (1..=64).contains(&id.len())
46 && id
47 .chars()
48 .next()
49 .is_some_and(|character| character.is_ascii_alphanumeric())
50 && id
51 .chars()
52 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'))
53}
54
55fn valid_dimension(value: f64) -> bool {
56 value.is_finite() && (200.0..=4096.0).contains(&value)
57}
58
59#[derive(Clone, Debug, PartialEq)]
60pub enum WindowCommand {
61 Open(ScriptWindowSpec),
62 Focus(String),
63 Close(String),
64}
65
66impl WindowCommand {
67 fn target(&self) -> &str {
68 match self {
69 Self::Open(spec) => &spec.id,
70 Self::Focus(id) | Self::Close(id) => id,
71 }
72 }
73}
74
75#[derive(Clone, Debug)]
80pub struct QueuedWindowCommand {
81 command: WindowCommand,
82 origin: WindowCommandOrigin,
83 target: Rc<WindowIdentity>,
84}
85
86impl QueuedWindowCommand {
87 #[must_use]
88 pub const fn command(&self) -> &WindowCommand {
89 &self.command
90 }
91
92 pub(crate) fn source_binding(&self) -> Option<NativeWindowBinding> {
93 match &self.origin {
94 WindowCommandOrigin::Host => None,
95 WindowCommandOrigin::View { identity, .. } => identity.native.borrow().clone(),
96 }
97 }
98
99 pub(crate) fn is_host_origin(&self) -> bool {
100 matches!(self.origin, WindowCommandOrigin::Host)
101 }
102
103 pub(crate) fn target_binding(&self) -> Option<NativeWindowBinding> {
104 self.target.native.borrow().clone()
105 }
106}
107
108#[derive(Clone, Debug)]
109enum WindowCommandOrigin {
110 Host,
111 View {
112 id: String,
113 identity: Rc<WindowIdentity>,
114 },
115}
116
117#[derive(Debug, Default)]
118struct WindowIdentity {
119 mount: RefCell<Option<Weak<()>>>,
120 native: RefCell<Option<NativeWindowBinding>>,
121}
122
123#[derive(Clone, Debug)]
124pub(crate) struct NativeWindowBinding {
125 pub(crate) window: gpui::WindowId,
126 pub(crate) lease: Weak<()>,
127}
128
129#[derive(Clone, Copy, Debug, Eq, PartialEq)]
130pub enum WindowCommandPolicy {
131 ApplicationOwned,
132 Disabled,
133}
134
135#[derive(Clone, Copy, Debug, Eq, PartialEq)]
136enum WindowStatus {
137 Pending,
138 Open,
139}
140
141#[derive(Clone, Debug)]
142struct WindowRecord {
143 status: WindowStatus,
144 policy: WindowCommandPolicy,
145 view_id: String,
146 close_handler: Option<ScriptCallback>,
147 identity: Rc<WindowIdentity>,
148}
149
150#[derive(Clone, Debug, Default)]
151pub struct WindowCommandRegistry {
152 windows: BTreeMap<String, WindowRecord>,
153 commands: VecDeque<QueuedWindowCommand>,
154}
155
156impl WindowCommandRegistry {
157 #[must_use]
158 pub fn new() -> Self {
159 Self::default()
160 }
161
162 pub fn register_open(&mut self, id: impl Into<String>) -> Result<(), WindowCommandError> {
168 self.register_open_with_policy(id, WindowCommandPolicy::ApplicationOwned)
169 }
170
171 pub fn register_open_with_policy(
177 &mut self,
178 id: impl Into<String>,
179 policy: WindowCommandPolicy,
180 ) -> Result<(), WindowCommandError> {
181 let id = id.into();
182 self.register_open_for_view(id.clone(), policy, id)
183 }
184
185 pub fn register_open_for_view(
191 &mut self,
192 id: impl Into<String>,
193 policy: WindowCommandPolicy,
194 view_id: impl Into<String>,
195 ) -> Result<(), WindowCommandError> {
196 let id = id.into();
197 if !valid_window_id(&id) {
198 return Err(WindowCommandError::InvalidId(id));
199 }
200 if self.windows.contains_key(&id) {
201 return Err(WindowCommandError::Duplicate(id));
202 }
203 if self.windows.len() >= MAX_WINDOWS {
204 return Err(WindowCommandError::WindowLimit(MAX_WINDOWS));
205 }
206 self.windows.insert(
207 id,
208 WindowRecord {
209 status: WindowStatus::Open,
210 policy,
211 view_id: view_id.into(),
212 close_handler: None,
213 identity: Rc::new(WindowIdentity::default()),
214 },
215 );
216 Ok(())
217 }
218
219 pub fn request_open(&mut self, spec: ScriptWindowSpec) -> Result<(), WindowCommandError> {
226 self.enqueue_open(spec, WindowCommandOrigin::Host)
227 }
228
229 fn enqueue_open(
230 &mut self,
231 spec: ScriptWindowSpec,
232 origin: WindowCommandOrigin,
233 ) -> Result<(), WindowCommandError> {
234 spec.validate()?;
235 if self.windows.contains_key(&spec.id) {
236 return Err(WindowCommandError::Duplicate(spec.id));
237 }
238 if self.windows.len() >= MAX_WINDOWS {
239 return Err(WindowCommandError::WindowLimit(MAX_WINDOWS));
240 }
241 self.require_queue_capacity()?;
242 let identity = Rc::new(WindowIdentity::default());
243 self.windows.insert(
244 spec.id.clone(),
245 WindowRecord {
246 status: WindowStatus::Pending,
247 policy: WindowCommandPolicy::ApplicationOwned,
248 view_id: spec.id.clone(),
249 close_handler: None,
250 identity: Rc::clone(&identity),
251 },
252 );
253 self.commands.push_back(QueuedWindowCommand {
254 command: WindowCommand::Open(spec),
255 origin,
256 target: identity,
257 });
258 Ok(())
259 }
260
261 pub fn mark_open(&mut self, id: &str) -> Result<(), WindowCommandError> {
267 let record = self
268 .windows
269 .get_mut(id)
270 .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?;
271 if record.status != WindowStatus::Pending {
272 return Err(WindowCommandError::AlreadyOpen(id.to_owned()));
273 }
274 record.status = WindowStatus::Open;
275 Ok(())
276 }
277
278 pub fn fail_open(&mut self, id: &str) {
279 if self
280 .windows
281 .get(id)
282 .is_some_and(|record| record.status == WindowStatus::Pending)
283 {
284 self.windows.remove(id);
285 }
286 }
287
288 pub fn request_focus(&mut self, id: &str) -> Result<(), WindowCommandError> {
295 self.enqueue_target(
296 WindowCommand::Focus(id.to_owned()),
297 WindowCommandOrigin::Host,
298 )
299 }
300
301 pub fn request_focus_from(&mut self, source: &str, id: &str) -> Result<(), WindowCommandError> {
307 self.require_commands(source, "focus_window")?;
308 self.enqueue_target(
309 WindowCommand::Focus(id.to_owned()),
310 self.view_origin(source),
311 )
312 }
313
314 pub fn request_close(&mut self, id: &str) -> Result<(), WindowCommandError> {
320 self.enqueue_target(
321 WindowCommand::Close(id.to_owned()),
322 WindowCommandOrigin::Host,
323 )
324 }
325
326 pub fn request_close_from(&mut self, source: &str, id: &str) -> Result<(), WindowCommandError> {
332 self.require_commands(source, "close_window")?;
333 self.enqueue_target(
334 WindowCommand::Close(id.to_owned()),
335 self.view_origin(source),
336 )
337 }
338
339 pub fn request_open_from(
345 &mut self,
346 source: &str,
347 spec: ScriptWindowSpec,
348 ) -> Result<(), WindowCommandError> {
349 self.require_commands(source, "open_window")?;
350 self.enqueue_open(spec, self.view_origin(source))
351 }
352
353 pub fn set_close_handler(
359 &mut self,
360 id: &str,
361 handler: Option<ScriptCallback>,
362 ) -> Result<(), WindowCommandError> {
363 self.require_commands(id, "set_close_handler")?;
364 let record = self
365 .windows
366 .get_mut(id)
367 .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?;
368 record.close_handler = handler;
369 Ok(())
370 }
371
372 #[must_use]
373 pub fn close_handler(&self, id: &str) -> Option<ScriptCallback> {
374 self.windows
375 .get(id)
376 .and_then(|record| record.close_handler.clone())
377 }
378
379 #[must_use]
380 pub fn drain_commands(&mut self) -> Vec<QueuedWindowCommand> {
381 self.commands.drain(..).collect()
382 }
383
384 #[must_use]
385 pub fn contains(&self, id: &str) -> bool {
386 self.windows.contains_key(id)
387 }
388
389 #[must_use]
390 pub fn open_ids(&self) -> Vec<String> {
391 self.windows
392 .iter()
393 .filter(|(_, record)| record.status == WindowStatus::Open)
394 .map(|(id, _)| id.clone())
395 .collect()
396 }
397
398 pub fn remove(&mut self, id: &str) -> bool {
399 let Some(removed) = self.windows.remove(id) else {
400 return false;
401 };
402 let queued = std::mem::take(&mut self.commands);
403 for command in queued {
404 let revoked = matches!(&command.origin, WindowCommandOrigin::View { identity, .. }
405 if Rc::ptr_eq(identity, &removed.identity));
406 if revoked || Rc::ptr_eq(&command.target, &removed.identity) {
407 self.cancel_open(&command);
408 } else {
409 self.commands.push_back(command);
410 }
411 }
412 true
413 }
414
415 fn view_origin(&self, id: &str) -> WindowCommandOrigin {
416 WindowCommandOrigin::View {
417 id: id.to_owned(),
418 identity: Rc::clone(&self.windows[id].identity),
419 }
420 }
421
422 fn enqueue_target(
423 &mut self,
424 command: WindowCommand,
425 origin: WindowCommandOrigin,
426 ) -> Result<(), WindowCommandError> {
427 let target = self
428 .windows
429 .get(command.target())
430 .ok_or_else(|| WindowCommandError::Unknown(command.target().to_owned()))?;
431 let identity = Rc::clone(&target.identity);
432 self.require_queue_capacity()?;
433 self.commands.push_back(QueuedWindowCommand {
434 command,
435 origin,
436 target: identity,
437 });
438 Ok(())
439 }
440
441 pub(crate) fn qualify_mount(
442 &mut self,
443 id: &str,
444 lease: &Weak<()>,
445 ) -> Result<(), WindowCommandError> {
446 let identity = &self
447 .windows
448 .get(id)
449 .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?
450 .identity;
451 let mut mount = identity.mount.borrow_mut();
452 if mount
453 .as_ref()
454 .is_some_and(|previous| !previous.ptr_eq(lease))
455 {
456 return Err(WindowCommandError::Duplicate(id.to_owned()));
457 }
458 *mount = Some(lease.clone());
459 Ok(())
460 }
461
462 pub(crate) fn bind_native(
463 &mut self,
464 id: &str,
465 window: gpui::WindowId,
466 ) -> Result<(), WindowCommandError> {
467 let identity = &self
468 .windows
469 .get(id)
470 .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?
471 .identity;
472 let lease = identity
473 .mount
474 .borrow()
475 .clone()
476 .ok_or_else(|| WindowCommandError::Unknown(id.to_owned()))?;
477 let mut native = identity.native.borrow_mut();
478 if native
479 .as_ref()
480 .is_some_and(|previous| previous.window != window || !previous.lease.ptr_eq(&lease))
481 {
482 return Err(WindowCommandError::Duplicate(id.to_owned()));
483 }
484 *native = Some(NativeWindowBinding { window, lease });
485 Ok(())
486 }
487
488 pub(crate) fn is_current_target(&self, command: &QueuedWindowCommand) -> bool {
489 self.windows
490 .get(command.command.target())
491 .is_some_and(|record| Rc::ptr_eq(&record.identity, &command.target))
492 }
493
494 pub(crate) fn is_current_origin(&self, command: &QueuedWindowCommand) -> bool {
495 match &command.origin {
496 WindowCommandOrigin::Host => true,
497 WindowCommandOrigin::View { id, identity } => {
498 self.windows.get(id).is_some_and(|record| {
499 Rc::ptr_eq(&record.identity, identity)
500 && identity
501 .mount
502 .borrow()
503 .as_ref()
504 .is_some_and(|lease| lease.strong_count() > 0)
505 })
506 }
507 }
508 }
509
510 pub(crate) fn cancel_open(&mut self, command: &QueuedWindowCommand) {
511 if matches!(command.command, WindowCommand::Open(_))
512 && self.is_current_target(command)
513 && self.windows[command.command.target()].status == WindowStatus::Pending
514 {
515 self.windows.remove(command.command.target());
516 }
517 }
518
519 fn require_commands(
520 &self,
521 source: &str,
522 command: &'static str,
523 ) -> Result<(), WindowCommandError> {
524 let record = self
525 .windows
526 .get(source)
527 .ok_or_else(|| WindowCommandError::Unknown(source.to_owned()))?;
528 if record.policy == WindowCommandPolicy::Disabled {
529 Err(WindowCommandError::UnsupportedInEmbeddedView {
530 window: source.to_owned(),
531 view: record.view_id.clone(),
532 command,
533 })
534 } else {
535 Ok(())
536 }
537 }
538
539 fn require_queue_capacity(&self) -> Result<(), WindowCommandError> {
540 if self.commands.len() < MAX_PENDING_COMMANDS {
541 Ok(())
542 } else {
543 Err(WindowCommandError::QueueLimit(MAX_PENDING_COMMANDS))
544 }
545 }
546}
547
548#[derive(Clone, Debug, Error, PartialEq)]
549pub enum WindowCommandError {
550 #[error("invalid window ID `{0}`")]
551 InvalidId(String),
552 #[error("window title must contain 1 to 256 characters")]
553 InvalidTitle,
554 #[error("window bounds must be finite and between 200 and 4096, got {width}x{height}")]
555 InvalidBounds { width: f64, height: f64 },
556 #[error("window `{0}` is already registered")]
557 Duplicate(String),
558 #[error("window `{0}` is not registered")]
559 Unknown(String),
560 #[error("window `{0}` is already open")]
561 AlreadyOpen(String),
562 #[error("at most {0} script windows may be active")]
563 WindowLimit(usize),
564 #[error("at most {0} window commands may be pending")]
565 QueueLimit(usize),
566 #[error(
567 "window command `{command}` is unavailable for embedded view `{view}` in window `{window}`"
568 )]
569 UnsupportedInEmbeddedView {
570 window: String,
571 view: String,
572 command: &'static str,
573 },
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579
580 fn spec(id: &str) -> ScriptWindowSpec {
581 ScriptWindowSpec {
582 id: id.to_owned(),
583 title: "Settings".to_owned(),
584 width: 640.0,
585 height: 480.0,
586 focus: true,
587 }
588 }
589
590 #[test]
591 fn commands_validate_identity_lifecycle_and_duplicates() {
592 let mut windows = WindowCommandRegistry::new();
593 windows.register_open("main").unwrap();
594 windows.request_open(spec("settings")).unwrap();
595 assert!(matches!(
596 windows.request_open(spec("settings")),
597 Err(WindowCommandError::Duplicate(_))
598 ));
599 windows.request_focus("settings").unwrap();
600 windows.request_close("settings").unwrap();
601 assert_eq!(
602 windows
603 .drain_commands()
604 .into_iter()
605 .map(|queued| queued.command)
606 .collect::<Vec<_>>(),
607 vec![
608 WindowCommand::Open(spec("settings")),
609 WindowCommand::Focus("settings".into()),
610 WindowCommand::Close("settings".into())
611 ]
612 );
613 windows.mark_open("settings").unwrap();
614 windows.request_focus("settings").unwrap();
615 windows.request_close("settings").unwrap();
616 assert_eq!(
617 windows
618 .drain_commands()
619 .into_iter()
620 .map(|queued| queued.command)
621 .collect::<Vec<_>>(),
622 vec![
623 WindowCommand::Focus("settings".to_owned()),
624 WindowCommand::Close("settings".to_owned()),
625 ]
626 );
627 }
628
629 #[test]
630 fn queued_origin_and_target_keep_their_registration_identity() {
631 let mut windows = WindowCommandRegistry::new();
632 let lease = Rc::new(());
633 windows.register_open("source").unwrap();
634 windows
635 .qualify_mount("source", &Rc::downgrade(&lease))
636 .unwrap();
637 windows.request_open_from("source", spec("next")).unwrap();
638 windows.request_focus_from("source", "next").unwrap();
639 let queued = windows.drain_commands();
640 assert!(queued.iter().all(
641 |command| windows.is_current_origin(command) && windows.is_current_target(command)
642 ));
643 windows.remove("source");
644 windows.register_open("source").unwrap();
645 let replacement = Rc::new(());
646 windows
647 .qualify_mount("source", &Rc::downgrade(&replacement))
648 .unwrap();
649 assert!(
650 queued
651 .iter()
652 .all(|command| !windows.is_current_origin(command))
653 );
654 windows.cancel_open(&queued[0]);
655 windows.request_open_from("source", spec("next")).unwrap();
656 assert!(!windows.is_current_target(&queued[1]));
657 windows.cancel_open(&queued[0]);
658 assert!(
659 windows.contains("next"),
660 "old cancellation must not release a newer reservation"
661 );
662 }
663
664 #[test]
665 fn revocation_releases_only_its_queued_open_and_host_origin_is_explicit() {
666 let mut windows = WindowCommandRegistry::new();
667 let lease = Rc::new(());
668 windows.register_open("source").unwrap();
669 windows
670 .qualify_mount("source", &Rc::downgrade(&lease))
671 .unwrap();
672 windows
673 .request_open_from("source", spec("cancelled"))
674 .unwrap();
675 windows.request_open(spec("trusted")).unwrap();
676 windows.remove("source");
677 assert!(!windows.contains("cancelled"));
678 let queued = windows.drain_commands();
679 assert_eq!(queued.len(), 1);
680 assert!(queued[0].is_host_origin());
681 assert!(windows.is_current_origin(&queued[0]));
682 assert!(windows.is_current_target(&queued[0]));
683 }
684
685 #[test]
686 fn invalid_window_specs_never_reserve_an_id() {
687 let mut windows = WindowCommandRegistry::new();
688 let mut invalid = spec("../escape");
689 invalid.width = 20.0;
690 assert!(windows.request_open(invalid).is_err());
691 assert!(!windows.contains("../escape"));
692 }
693
694 #[test]
695 fn embedded_view_policy_rejects_window_commands_at_call_site() {
696 let mut windows = WindowCommandRegistry::new();
697 windows
698 .register_open_with_policy("host", WindowCommandPolicy::Disabled)
699 .unwrap();
700 assert!(matches!(
701 windows.request_open_from("host", spec("settings")),
702 Err(WindowCommandError::UnsupportedInEmbeddedView {
703 command: "open_window",
704 ..
705 })
706 ));
707 assert!(matches!(
708 windows.request_focus_from("host", "host"),
709 Err(WindowCommandError::UnsupportedInEmbeddedView {
710 command: "focus_window",
711 ..
712 })
713 ));
714 assert!(matches!(
715 windows.request_close_from("host", "host"),
716 Err(WindowCommandError::UnsupportedInEmbeddedView {
717 command: "close_window",
718 ..
719 })
720 ));
721 assert!(windows.drain_commands().is_empty());
722 }
723}