1use std::future::{Future, IntoFuture};
8use std::pin::Pin;
9use std::time::Duration;
10
11use tokio::task::JoinSet;
12
13use crate::{
14 BroadcastResult, Input, Pane, PaneCloseOutcome, PaneId, PaneRef, PaneSnapshot, Result,
15 RmuxError,
16};
17
18#[derive(Debug, Clone, Default)]
20#[must_use = "pane sets do nothing unless one of their async methods is awaited"]
21pub struct PaneSet {
22 panes: Vec<Pane>,
23}
24
25impl PaneSet {
26 pub fn new<I>(panes: I) -> Self
31 where
32 I: IntoIterator<Item = Pane>,
33 {
34 Self {
35 panes: panes.into_iter().collect(),
36 }
37 }
38
39 #[must_use]
41 pub fn panes(&self) -> &[Pane] {
42 &self.panes
43 }
44
45 #[must_use]
47 pub fn len(&self) -> usize {
48 self.panes.len()
49 }
50
51 #[must_use]
53 pub fn is_empty(&self) -> bool {
54 self.panes.is_empty()
55 }
56
57 pub async fn broadcast(&self, input: Input<'_>) -> Result<BroadcastResult> {
62 crate::broadcast::broadcast(&self.panes, input).await
63 }
64
65 pub async fn snapshot_all(&self) -> PaneSetBatch<PaneSnapshot> {
71 run_all(
72 self.panes.clone(),
73 |pane| async move { pane.snapshot().await },
74 )
75 .await
76 }
77
78 pub async fn close_all(self) -> PaneSetBatch<PaneCloseOutcome> {
83 run_all(self.panes, |pane| async move { pane.close().await }).await
84 }
85
86 #[must_use]
88 pub fn expect_all(&self) -> PaneSetExpectation<'_> {
89 PaneSetExpectation {
90 panes: &self.panes,
91 mode: ExpectMode::All,
92 }
93 }
94
95 #[must_use]
97 pub fn expect_any(&self) -> PaneSetExpectation<'_> {
98 PaneSetExpectation {
99 panes: &self.panes,
100 mode: ExpectMode::Any,
101 }
102 }
103
104 #[must_use]
106 pub fn wait_all(&self) -> PaneSetExpectation<'_> {
107 self.expect_all()
108 }
109
110 #[must_use]
112 pub fn wait_any(&self) -> PaneSetExpectation<'_> {
113 self.expect_any()
114 }
115}
116
117impl From<Vec<Pane>> for PaneSet {
118 fn from(panes: Vec<Pane>) -> Self {
119 Self { panes }
120 }
121}
122
123impl FromIterator<Pane> for PaneSet {
124 fn from_iter<T: IntoIterator<Item = Pane>>(iter: T) -> Self {
125 Self::new(iter)
126 }
127}
128
129impl IntoIterator for PaneSet {
130 type Item = Pane;
131 type IntoIter = std::vec::IntoIter<Pane>;
132
133 fn into_iter(self) -> Self::IntoIter {
134 self.panes.into_iter()
135 }
136}
137
138#[derive(Debug)]
140pub struct PaneSetSuccess<T> {
141 target: PaneRef,
142 pane_id: Option<PaneId>,
143 value: T,
144}
145
146impl<T> PaneSetSuccess<T> {
147 fn new(target: PaneRef, pane_id: Option<PaneId>, value: T) -> Self {
148 Self {
149 target,
150 pane_id,
151 value,
152 }
153 }
154
155 #[must_use]
157 pub const fn target(&self) -> &PaneRef {
158 &self.target
159 }
160
161 #[must_use]
163 pub const fn pane_id(&self) -> Option<PaneId> {
164 self.pane_id
165 }
166
167 #[must_use]
169 pub const fn value(&self) -> &T {
170 &self.value
171 }
172
173 pub fn into_value(self) -> T {
175 self.value
176 }
177}
178
179#[derive(Debug)]
181pub struct PaneSetFailure {
182 target: PaneRef,
183 pane_id: Option<PaneId>,
184 error: RmuxError,
185}
186
187impl PaneSetFailure {
188 fn new(target: PaneRef, pane_id: Option<PaneId>, error: RmuxError) -> Self {
189 Self {
190 target,
191 pane_id,
192 error,
193 }
194 }
195
196 #[must_use]
198 pub const fn target(&self) -> &PaneRef {
199 &self.target
200 }
201
202 #[must_use]
204 pub const fn pane_id(&self) -> Option<PaneId> {
205 self.pane_id
206 }
207
208 #[must_use]
210 pub const fn error(&self) -> &RmuxError {
211 &self.error
212 }
213
214 pub fn into_error(self) -> RmuxError {
216 self.error
217 }
218}
219
220#[derive(Debug)]
222pub struct PaneSetBatch<T> {
223 successes: Vec<PaneSetSuccess<T>>,
224 failures: Vec<PaneSetFailure>,
225}
226
227impl<T> PaneSetBatch<T> {
228 fn new(successes: Vec<PaneSetSuccess<T>>, failures: Vec<PaneSetFailure>) -> Self {
229 Self {
230 successes,
231 failures,
232 }
233 }
234
235 #[must_use]
237 pub fn is_success(&self) -> bool {
238 self.failures.is_empty()
239 }
240
241 #[must_use]
243 pub fn successes(&self) -> &[PaneSetSuccess<T>] {
244 &self.successes
245 }
246
247 #[must_use]
249 pub fn failures(&self) -> &[PaneSetFailure] {
250 &self.failures
251 }
252
253 #[must_use]
255 pub fn len(&self) -> usize {
256 self.successes.len() + self.failures.len()
257 }
258
259 #[must_use]
261 pub fn is_empty(&self) -> bool {
262 self.successes.is_empty() && self.failures.is_empty()
263 }
264}
265
266#[derive(Debug)]
268pub struct PaneSetAny<T> {
269 success: Option<PaneSetSuccess<T>>,
270 failures: Vec<PaneSetFailure>,
271}
272
273impl<T> PaneSetAny<T> {
274 fn from_success(success: PaneSetSuccess<T>, failures: Vec<PaneSetFailure>) -> Self {
275 Self {
276 success: Some(success),
277 failures,
278 }
279 }
280
281 fn failure(failures: Vec<PaneSetFailure>) -> Self {
282 Self {
283 success: None,
284 failures,
285 }
286 }
287
288 #[must_use]
290 pub fn matched(&self) -> bool {
291 self.success.is_some()
292 }
293
294 #[must_use]
296 pub const fn success(&self) -> Option<&PaneSetSuccess<T>> {
297 self.success.as_ref()
298 }
299
300 #[must_use]
303 pub fn failures(&self) -> &[PaneSetFailure] {
304 &self.failures
305 }
306}
307
308#[derive(Debug, Clone, Copy)]
310pub struct PaneSetExpectation<'a> {
311 panes: &'a [Pane],
312 mode: ExpectMode,
313}
314
315impl<'a> PaneSetExpectation<'a> {
316 pub fn visible_text_matches_any<I, S>(self, patterns: I) -> PaneSetVisibleTextWait<'a>
318 where
319 I: IntoIterator<Item = S>,
320 S: Into<String>,
321 {
322 PaneSetVisibleTextWait::new(
323 self.panes,
324 self.mode,
325 VisibleSetMatcher::Any(patterns.into_iter().map(Into::into).collect()),
326 )
327 }
328
329 pub fn visible_text_matches_all<I, S>(self, patterns: I) -> PaneSetVisibleTextWait<'a>
332 where
333 I: IntoIterator<Item = S>,
334 S: Into<String>,
335 {
336 PaneSetVisibleTextWait::new(
337 self.panes,
338 self.mode,
339 VisibleSetMatcher::All(patterns.into_iter().map(Into::into).collect()),
340 )
341 }
342
343 pub fn visible_text_contains(self, pattern: impl Into<String>) -> PaneSetVisibleTextWait<'a> {
346 PaneSetVisibleTextWait::new(
347 self.panes,
348 self.mode,
349 VisibleSetMatcher::Contains(pattern.into()),
350 )
351 }
352}
353
354#[derive(Debug)]
356#[must_use = "pane-set visible waits do nothing unless awaited"]
357pub struct PaneSetVisibleTextWait<'a> {
358 panes: &'a [Pane],
359 mode: ExpectMode,
360 matcher: VisibleSetMatcher,
361 timeout: Option<Duration>,
362 poll_interval: Option<Duration>,
363}
364
365impl<'a> PaneSetVisibleTextWait<'a> {
366 fn new(panes: &'a [Pane], mode: ExpectMode, matcher: VisibleSetMatcher) -> Self {
367 Self {
368 panes,
369 mode,
370 matcher,
371 timeout: None,
372 poll_interval: None,
373 }
374 }
375
376 pub const fn timeout(mut self, timeout: Duration) -> Self {
378 self.timeout = Some(timeout);
379 self
380 }
381
382 pub const fn poll_interval(mut self, interval: Duration) -> Self {
384 self.poll_interval = Some(interval);
385 self
386 }
387
388 async fn run(self) -> PaneSetVisibleTextOutcome {
389 match self.mode {
390 ExpectMode::All => {
391 let matcher = self.matcher;
392 let timeout = self.timeout;
393 let poll_interval = self.poll_interval;
394 PaneSetVisibleTextOutcome::All(
395 run_all(self.panes.to_vec(), move |pane| {
396 let matcher = matcher.clone();
397 async move { wait_visible_text(pane, matcher, timeout, poll_interval).await }
398 })
399 .await,
400 )
401 }
402 ExpectMode::Any => PaneSetVisibleTextOutcome::Any(self.run_any().await),
403 }
404 }
405
406 async fn run_any(self) -> PaneSetAny<PaneSnapshot> {
407 let mut tasks = JoinSet::new();
408 for pane in self.panes.iter().cloned() {
409 let matcher = self.matcher.clone();
410 let timeout = self.timeout;
411 let poll_interval = self.poll_interval;
412 tasks.spawn(async move {
413 let target = pane.target().clone();
414 let pane_id = pane.id().await.ok().flatten();
415 let result = wait_visible_text(pane, matcher, timeout, poll_interval).await;
416 (target, pane_id, result)
417 });
418 }
419
420 let mut failures = Vec::new();
421 while let Some(joined) = tasks.join_next().await {
422 let (target, pane_id, result) = match joined {
423 Ok(outcome) => outcome,
424 Err(error) => {
425 failures.push(join_failure(error));
426 continue;
427 }
428 };
429 match result {
430 Ok(snapshot) => {
431 tasks.abort_all();
432 return PaneSetAny::from_success(
433 PaneSetSuccess::new(target, pane_id, snapshot),
434 failures,
435 );
436 }
437 Err(error) => failures.push(PaneSetFailure::new(target, pane_id, error)),
438 }
439 }
440 PaneSetAny::failure(failures)
441 }
442}
443
444impl<'a> IntoFuture for PaneSetVisibleTextWait<'a> {
445 type Output = PaneSetVisibleTextOutcome;
446 type IntoFuture = Pin<Box<dyn Future<Output = Self::Output> + Send + 'a>>;
447
448 fn into_future(self) -> Self::IntoFuture {
449 Box::pin(self.run())
450 }
451}
452
453#[derive(Debug)]
455#[non_exhaustive]
456pub enum PaneSetVisibleTextOutcome {
457 All(PaneSetBatch<PaneSnapshot>),
459 Any(PaneSetAny<PaneSnapshot>),
461}
462
463impl PaneSetVisibleTextOutcome {
464 #[must_use]
467 pub const fn all(&self) -> Option<&PaneSetBatch<PaneSnapshot>> {
468 match self {
469 Self::All(batch) => Some(batch),
470 Self::Any(_) => None,
471 }
472 }
473
474 #[must_use]
477 pub const fn any(&self) -> Option<&PaneSetAny<PaneSnapshot>> {
478 match self {
479 Self::Any(result) => Some(result),
480 Self::All(_) => None,
481 }
482 }
483}
484
485#[derive(Debug, Clone, Copy)]
486enum ExpectMode {
487 All,
488 Any,
489}
490
491#[derive(Debug, Clone)]
492enum VisibleSetMatcher {
493 Contains(String),
494 Any(Vec<String>),
495 All(Vec<String>),
496}
497
498async fn wait_visible_text(
499 pane: Pane,
500 matcher: VisibleSetMatcher,
501 timeout: Option<Duration>,
502 poll_interval: Option<Duration>,
503) -> Result<PaneSnapshot> {
504 match matcher {
505 VisibleSetMatcher::Contains(pattern) => {
506 let wait = pane.expect_visible_text().to_contain(pattern);
507 apply_visible_options(wait, timeout, poll_interval).await
508 }
509 VisibleSetMatcher::Any(patterns) => {
510 let wait = pane.expect_visible_text().to_match_any(patterns);
511 apply_visible_options(wait, timeout, poll_interval).await
512 }
513 VisibleSetMatcher::All(patterns) => {
514 let wait = pane.expect_visible_text().to_match_all(patterns);
515 apply_visible_options(wait, timeout, poll_interval).await
516 }
517 }
518}
519
520async fn apply_visible_options(
521 mut wait: crate::VisibleTextWait<'_>,
522 timeout: Option<Duration>,
523 poll_interval: Option<Duration>,
524) -> Result<PaneSnapshot> {
525 if let Some(timeout) = timeout {
526 wait = wait.timeout(timeout);
527 }
528 if let Some(poll_interval) = poll_interval {
529 wait = wait.poll_interval(poll_interval);
530 }
531 wait.await
532}
533
534async fn run_all<T, Fut>(
535 panes: Vec<Pane>,
536 operation: impl Fn(Pane) -> Fut + Clone + Send + Sync + 'static,
537) -> PaneSetBatch<T>
538where
539 T: Send + 'static,
540 Fut: Future<Output = Result<T>> + Send + 'static,
541{
542 let mut tasks = JoinSet::new();
543 for (index, pane) in panes.into_iter().enumerate() {
544 let operation = operation.clone();
545 tasks.spawn(async move {
546 let target = pane.target().clone();
547 let pane_id = pane.id().await.ok().flatten();
548 let result = operation(pane).await;
549 (index, target, pane_id, result)
550 });
551 }
552
553 let mut outcomes = Vec::new();
554 while let Some(joined) = tasks.join_next().await {
555 match joined {
556 Ok(outcome) => outcomes.push(outcome),
557 Err(error) => outcomes.push((
558 usize::MAX,
559 PaneRef::new(
560 crate::SessionName::new("unknown").expect("static session name"),
561 0,
562 0,
563 ),
564 None,
565 Err(RmuxError::transport(
566 "join pane-set worker task",
567 std::io::Error::other(error.to_string()),
568 )),
569 )),
570 }
571 }
572 outcomes.sort_by_key(|(index, _, _, _)| *index);
573
574 let mut successes = Vec::new();
575 let mut failures = Vec::new();
576 for (_, target, pane_id, result) in outcomes {
577 match result {
578 Ok(value) => successes.push(PaneSetSuccess::new(target, pane_id, value)),
579 Err(error) => failures.push(PaneSetFailure::new(target, pane_id, error)),
580 }
581 }
582 PaneSetBatch::new(successes, failures)
583}
584
585fn join_failure(error: tokio::task::JoinError) -> PaneSetFailure {
586 PaneSetFailure::new(
587 PaneRef::new(
588 crate::SessionName::new("unknown").expect("static session name"),
589 0,
590 0,
591 ),
592 None,
593 RmuxError::transport(
594 "join pane-set worker task",
595 std::io::Error::other(error.to_string()),
596 ),
597 )
598}