1use rmux_proto::{RmuxError, SessionName, Target};
12
13use crate::SessionStore;
14
15#[path = "target_find/current.rs"]
16mod current;
17#[path = "target_find/matching.rs"]
18mod matching;
19#[path = "target_find/metadata.rs"]
20mod metadata;
21#[path = "target_find/pane_navigation.rs"]
22mod pane_navigation;
23#[path = "target_find/session.rs"]
24mod session;
25#[path = "target_find/syntax.rs"]
26mod syntax;
27#[path = "target_find/types.rs"]
28mod types;
29
30use matching::{
31 cyclic_pane_offset, cyclic_window_offset, find_pane_id_in_session, offset_index, parse_offset,
32 parse_required_prefixed_id, special_window_index, target_for_type, target_not_found,
33 unique_match, unique_window_name_match, MatchMode, ResolvedParts,
34};
35pub use metadata::command_target_metadata;
36use pane_navigation::{directional_pane, pane_description};
37use syntax::TargetParts;
38pub use types::{
39 CommandTargetMetadata, CommandTargetSpec, MissingCurrentTargetFallback, TargetFindContext,
40 TargetFindFlags, TargetFindType, UnresolvedTarget,
41};
42
43impl SessionStore {
44 pub fn resolve_unresolved_target(
46 &self,
47 target: &UnresolvedTarget,
48 find_type: TargetFindType,
49 flags: TargetFindFlags,
50 context: &TargetFindContext,
51 ) -> Result<Target, RmuxError> {
52 let Some(raw) = target.as_deref() else {
53 return self.current_target_for_type(find_type, flags, context);
54 };
55 if raw.is_empty() {
56 return self.current_target_for_type(find_type, flags, context);
57 }
58
59 if raw == "." {
60 return self.current_target_for_type(find_type, flags, context);
61 }
62 if matches!(raw, "{active}" | "{current}") {
63 return self.current_target_for_type(find_type, flags, context);
64 }
65 if matches!(raw, "=" | "{mouse}") {
66 return self.mouse_target_for_type(find_type, context);
67 }
68 if matches!(raw, "~" | "{marked}") {
69 return self.marked_target_for_type(find_type, context);
70 }
71
72 let mut parts = TargetParts::parse(raw, find_type);
73 let mut flags = flags;
74 parts.apply_exact_prefixes(&mut flags);
75 if flags.contains(TargetFindFlags::WINDOW_INDEX)
76 && parts.window_only
77 && parts.window == Some("")
78 && parts.pane.is_none()
79 {
80 return self.resolve_empty_window_index_target(
81 raw,
82 parts.session,
83 find_type,
84 flags,
85 context,
86 );
87 }
88 parts.drop_empty();
89 parts.map_tokens();
90 if flags.contains(TargetFindFlags::WINDOW_INDEX) && parts.pane.is_some() {
91 return Err(RmuxError::invalid_target(raw, "can't specify pane here"));
92 }
93
94 self.resolve_parts(raw, parts, find_type, flags, context)
95 }
96
97 fn resolve_parts(
98 &self,
99 raw: &str,
100 parts: TargetParts<'_>,
101 find_type: TargetFindType,
102 flags: TargetFindFlags,
103 context: &TargetFindContext,
104 ) -> Result<Target, RmuxError> {
105 if let Some(session) = parts.session {
106 let session_name = self.resolve_session_part(session, flags)?;
107 if parts.window.is_none() && parts.pane.is_none() {
108 return self.target_for_session_current(find_type, session_name);
109 }
110 if let Some(window) = parts.window {
111 let window = self.resolve_window_in_session(raw, &session_name, window, flags)?;
112 if let Some(pane) = parts.pane {
113 let pane = self.resolve_pane_in_window(
114 raw,
115 &window.session_name,
116 window.window_index,
117 pane,
118 context,
119 )?;
120 return Ok(target_for_type(find_type, pane));
121 }
122 return self.target_for_window_current(find_type, window);
123 }
124
125 let pane = self.resolve_pane_in_session(
126 raw,
127 &session_name,
128 parts.pane.expect("pane branch requires pane"),
129 context,
130 )?;
131 return Ok(target_for_type(find_type, pane));
132 }
133
134 if let Some(window) = parts.window {
135 let window =
136 self.resolve_window_from_context(raw, window, parts.window_only, flags, context)?;
137 if let Some(pane) = parts.pane {
138 let pane = self.resolve_pane_in_window(
139 raw,
140 &window.session_name,
141 window.window_index,
142 pane,
143 context,
144 )?;
145 return Ok(target_for_type(find_type, pane));
146 }
147 return self.target_for_window_current(find_type, window);
148 }
149
150 if let Some(pane) = parts.pane {
151 let pane =
152 self.resolve_pane_from_context(raw, pane, parts.pane_only, flags, context)?;
153 return Ok(target_for_type(find_type, pane));
154 }
155
156 self.current_target_for_type(find_type, flags, context)
157 }
158
159 fn resolve_empty_window_index_target(
160 &self,
161 raw: &str,
162 session_part: Option<&str>,
163 find_type: TargetFindType,
164 flags: TargetFindFlags,
165 context: &TargetFindContext,
166 ) -> Result<Target, RmuxError> {
167 let session_name = match session_part.filter(|value| !value.is_empty()) {
168 Some(session_part) => self.resolve_session_part(session_part, flags)?,
169 None => self
170 .current_parts(context)
171 .map(|parts| parts.session_name)
172 .or_else(|_| self.default_session_name(flags))?,
173 };
174 let session = self
175 .session(&session_name)
176 .ok_or_else(|| target_not_found(raw, "session"))?;
177 let window_index = session.lowest_available_window_index_at_or_above(0)?;
178 Ok(target_for_type(
179 find_type,
180 ResolvedParts::window(session_name, window_index),
181 ))
182 }
183
184 fn resolve_window_from_context(
185 &self,
186 raw: &str,
187 value: &str,
188 only_window: bool,
189 flags: TargetFindFlags,
190 context: &TargetFindContext,
191 ) -> Result<ResolvedParts, RmuxError> {
192 if value.starts_with('@') {
193 return self.resolve_window_id(raw, value, context);
194 }
195
196 let current = match self.current_parts(context) {
197 Ok(current) => current,
198 Err(error) if !only_window => {
199 if let Ok(window) = self.resolve_window_globally(raw, value, flags) {
200 return Ok(window);
201 }
202 return self
203 .resolve_session_part(value, flags)
204 .and_then(|session_name| self.parts_for_session_current(&session_name))
205 .map_err(|_| error);
206 }
207 Err(_) if only_window => {
208 return self.resolve_window_globally(raw, value, flags);
209 }
210 Err(error) => return Err(error),
211 };
212 match self.resolve_window_in_session(raw, ¤t.session_name, value, flags) {
213 Ok(window) => Ok(window),
214 Err(error) if !only_window => match self.resolve_session_part(value, flags) {
215 Ok(session_name) => self.parts_for_session_current(&session_name),
216 Err(_) => Err(error),
217 },
218 Err(error) => Err(error),
219 }
220 }
221
222 fn resolve_window_globally(
223 &self,
224 raw: &str,
225 value: &str,
226 flags: TargetFindFlags,
227 ) -> Result<ResolvedParts, RmuxError> {
228 let mut exact = Vec::new();
229 for (session_name, _) in self.iter() {
230 if let Ok(window) = self.resolve_window_in_session(raw, session_name, value, flags) {
231 exact.push(window);
232 }
233 }
234 exact.sort_by(|left, right| {
235 left.session_name
236 .as_str()
237 .cmp(right.session_name.as_str())
238 .then(left.window_index.cmp(&right.window_index))
239 });
240 if let Some(window) = unique_match(exact, raw, "window")? {
241 return Ok(window);
242 }
243
244 Err(target_not_found(value, "window"))
245 }
246
247 fn resolve_window_in_session(
248 &self,
249 raw: &str,
250 session_name: &SessionName,
251 value: &str,
252 flags: TargetFindFlags,
253 ) -> Result<ResolvedParts, RmuxError> {
254 let session = self
255 .session(session_name)
256 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?;
257
258 if value.starts_with('@') {
259 if value == "@" {
260 return Err(target_not_found(value, "window"));
261 }
262 let window_id = crate::WindowId::new(parse_required_prefixed_id(value, '@')?);
263 let Some(window_index) = session
264 .windows()
265 .iter()
266 .find_map(|(index, window)| (window.id() == window_id).then_some(*index))
267 else {
268 return Err(target_not_found(value, "window"));
269 };
270 return self.parts_for_window(session_name, window_index);
271 }
272
273 if flags.contains(TargetFindFlags::WINDOW_INDEX) {
274 if let Some(window_index) = resolve_absolute_window_index_slot(value)? {
275 return Ok(ResolvedParts::window(session_name.clone(), window_index));
276 }
277 }
278
279 if !flags.contains(TargetFindFlags::EXACT_WINDOW) {
280 if let Some(offset) = parse_offset(value)? {
281 let window_index = if flags.contains(TargetFindFlags::WINDOW_INDEX) {
282 offset_index(session.active_window_index(), offset)
283 .ok_or_else(|| target_not_found(value, "window"))?
284 } else {
285 cyclic_window_offset(session, offset)
286 .ok_or_else(|| target_not_found(value, "window"))?
287 };
288 return Ok(ResolvedParts::window(session_name.clone(), window_index));
289 }
290
291 if let Some(window_index) = special_window_index(session, value) {
292 return self.parts_for_window(session_name, window_index);
293 }
294 }
295
296 if !value.starts_with(['+', '-']) {
297 if let Ok(window_index) = value.parse::<u32>() {
298 if session.window_at(window_index).is_some() {
299 return self.parts_for_window(session_name, window_index);
300 }
301 if flags.contains(TargetFindFlags::WINDOW_INDEX) {
302 return Ok(ResolvedParts::window(session_name.clone(), window_index));
303 }
304 }
305 }
306
307 if let Some(window_index) = unique_window_name_match(session, value, MatchMode::Exact)? {
308 return self.parts_for_window(session_name, window_index);
309 }
310
311 if flags.contains(TargetFindFlags::EXACT_WINDOW) {
312 return Err(target_not_found(value, "window"));
313 }
314
315 if let Some(window_index) = unique_window_name_match(session, value, MatchMode::Prefix)? {
316 return self.parts_for_window(session_name, window_index);
317 }
318 if let Some(window_index) = unique_window_name_match(session, value, MatchMode::Pattern)? {
319 return self.parts_for_window(session_name, window_index);
320 }
321
322 Err(RmuxError::invalid_target(
323 raw,
324 format!("can't find window: {value}"),
325 ))
326 }
327
328 fn resolve_window_id(
329 &self,
330 raw: &str,
331 value: &str,
332 context: &TargetFindContext,
333 ) -> Result<ResolvedParts, RmuxError> {
334 if value == "@" {
335 return Err(target_not_found(value, "window"));
336 }
337 let window_id = crate::WindowId::new(parse_required_prefixed_id(value, '@')?);
338 if let Ok(current) = self.current_parts(context) {
339 if let Some(window_index) = self.session(¤t.session_name).and_then(|session| {
340 session
341 .windows()
342 .iter()
343 .find_map(|(index, window)| (window.id() == window_id).then_some(*index))
344 }) {
345 return Ok(ResolvedParts::window(current.session_name, window_index));
346 }
347 }
348
349 let mut matches = self
350 .iter()
351 .flat_map(|(session_name, session)| {
352 session
353 .windows()
354 .iter()
355 .filter(move |(_, window)| window.id() == window_id)
356 .map(move |(window_index, _)| {
357 ResolvedParts::window(session_name.clone(), *window_index)
358 })
359 })
360 .collect::<Vec<_>>();
361 matches.sort_by(|left, right| {
362 left.session_name
363 .as_str()
364 .cmp(right.session_name.as_str())
365 .then(left.window_index.cmp(&right.window_index))
366 });
367 unique_match(matches, raw, "window")?.ok_or_else(|| target_not_found(value, "window"))
368 }
369
370 fn resolve_pane_from_context(
371 &self,
372 raw: &str,
373 value: &str,
374 only_pane: bool,
375 flags: TargetFindFlags,
376 context: &TargetFindContext,
377 ) -> Result<ResolvedParts, RmuxError> {
378 if value.starts_with('%') {
379 return self.resolve_pane_id(raw, value, context);
380 }
381
382 let current = match self.current_parts(context) {
383 Ok(current) => current,
384 Err(error) if !only_pane => {
385 return self
386 .resolve_window_from_context(raw, value, false, flags, context)
387 .and_then(|window| {
388 self.target_for_window_current(TargetFindType::Pane, window)
389 .and_then(|target| {
390 if let Target::Pane(target) = target {
391 Ok(ResolvedParts::pane(
392 target.session_name().clone(),
393 target.window_index(),
394 target.pane_index(),
395 ))
396 } else {
397 Err(RmuxError::Server(
398 "pane fallback produced non-pane target".to_owned(),
399 ))
400 }
401 })
402 })
403 .map_err(|_| error);
404 }
405 Err(error) => return Err(error),
406 };
407 match self.resolve_pane_in_window(
408 raw,
409 ¤t.session_name,
410 current.window_index,
411 value,
412 context,
413 ) {
414 Ok(pane) => Ok(pane),
415 Err(error) if !only_pane => {
416 match self.resolve_window_from_context(raw, value, false, flags, context) {
417 Ok(window) => self
418 .target_for_window_current(TargetFindType::Pane, window)
419 .and_then(|target| {
420 if let Target::Pane(target) = target {
421 Ok(ResolvedParts::pane(
422 target.session_name().clone(),
423 target.window_index(),
424 target.pane_index(),
425 ))
426 } else {
427 Err(RmuxError::Server(
428 "pane fallback produced non-pane target".to_owned(),
429 ))
430 }
431 }),
432 Err(_) => Err(error),
433 }
434 }
435 Err(error) => Err(error),
436 }
437 }
438
439 fn resolve_pane_in_session(
440 &self,
441 raw: &str,
442 session_name: &SessionName,
443 value: &str,
444 context: &TargetFindContext,
445 ) -> Result<ResolvedParts, RmuxError> {
446 if value.starts_with('%') {
447 let pane_id = crate::PaneId::new(parse_required_prefixed_id(value, '%')?);
448 let session = self
449 .session(session_name)
450 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?;
451 let Some((window_index, pane_index)) = find_pane_id_in_session(session, pane_id) else {
452 return Err(target_not_found(value, "pane"));
453 };
454 return Ok(ResolvedParts::pane(
455 session_name.clone(),
456 window_index,
457 pane_index,
458 ));
459 }
460
461 let window_index = self
462 .session(session_name)
463 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?
464 .active_window_index();
465 self.resolve_pane_in_window(raw, session_name, window_index, value, context)
466 }
467
468 fn resolve_pane_in_window(
469 &self,
470 raw: &str,
471 session_name: &SessionName,
472 window_index: u32,
473 value: &str,
474 context: &TargetFindContext,
475 ) -> Result<ResolvedParts, RmuxError> {
476 let session = self
477 .session(session_name)
478 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?;
479 let window = session
480 .window_at(window_index)
481 .ok_or_else(|| target_not_found(&format!("{session_name}:{window_index}"), "window"))?;
482
483 if value.starts_with('%') {
484 let pane_id = crate::PaneId::new(parse_required_prefixed_id(value, '%')?);
485 let Some(pane) = window.panes().iter().find(|pane| pane.id() == pane_id) else {
486 return Err(target_not_found(value, "pane"));
487 };
488 return Ok(ResolvedParts::pane(
489 session_name.clone(),
490 window_index,
491 pane.index(),
492 ));
493 }
494
495 if value == "!" {
496 let pane_index = window
497 .last_pane_index()
498 .ok_or_else(|| target_not_found(value, "pane"))?;
499 return Ok(ResolvedParts::pane(
500 session_name.clone(),
501 window_index,
502 pane_index,
503 ));
504 }
505
506 if let Some(pane_index) = directional_pane(window, value) {
507 return Ok(ResolvedParts::pane(
508 session_name.clone(),
509 window_index,
510 pane_index,
511 ));
512 }
513
514 if let Some(offset) = parse_offset(value)? {
515 let pane_index = cyclic_pane_offset(window, offset)
516 .ok_or_else(|| target_not_found(value, "pane"))?;
517 return Ok(ResolvedParts::pane(
518 session_name.clone(),
519 window_index,
520 pane_index,
521 ));
522 }
523
524 if let Ok(visible_pane_index) = value.parse::<u32>() {
525 let pane_base_index = context.pane_base_index(session_name, window_index);
526 if let Some(pane_index) = visible_pane_index.checked_sub(pane_base_index) {
527 if window.pane(pane_index).is_some() {
528 return Ok(ResolvedParts::pane(
529 session_name.clone(),
530 window_index,
531 pane_index,
532 ));
533 }
534 }
535 if pane_base_index == 0 && window.pane(visible_pane_index).is_some() {
536 return Ok(ResolvedParts::pane(
537 session_name.clone(),
538 window_index,
539 visible_pane_index,
540 ));
541 }
542 }
543
544 if let Some(pane_index) = pane_description(window, value) {
545 return Ok(ResolvedParts::pane(
546 session_name.clone(),
547 window_index,
548 pane_index,
549 ));
550 }
551
552 Err(RmuxError::invalid_target(
553 raw,
554 format!("can't find pane: {value}"),
555 ))
556 }
557
558 fn resolve_pane_id(
559 &self,
560 raw: &str,
561 value: &str,
562 context: &TargetFindContext,
563 ) -> Result<ResolvedParts, RmuxError> {
564 let pane_id = crate::PaneId::new(parse_required_prefixed_id(value, '%')?);
565 if let Ok(current) = self.current_parts(context) {
566 if let Some(pane) = self
567 .session(¤t.session_name)
568 .and_then(|session| session.window_at(current.window_index))
569 .and_then(|window| window.pane(current.pane_index))
570 {
571 if pane.id() == pane_id {
572 return Ok(current);
573 }
574 }
575 }
576
577 let mut matches = self
578 .iter()
579 .flat_map(|(session_name, session)| {
580 session
581 .windows()
582 .iter()
583 .flat_map(move |(window_index, window)| {
584 window
585 .panes()
586 .iter()
587 .filter(move |pane| pane.id() == pane_id)
588 .map(move |pane| {
589 ResolvedParts::pane(
590 session_name.clone(),
591 *window_index,
592 pane.index(),
593 )
594 })
595 })
596 })
597 .collect::<Vec<_>>();
598 matches.sort_by(|left, right| {
599 left.session_name
600 .as_str()
601 .cmp(right.session_name.as_str())
602 .then(left.window_index.cmp(&right.window_index))
603 .then(left.pane_index.cmp(&right.pane_index))
604 });
605 unique_match(matches, raw, "pane")?.ok_or_else(|| target_not_found(value, "pane"))
606 }
607}
608
609fn resolve_absolute_window_index_slot(value: &str) -> Result<Option<u32>, RmuxError> {
610 if value.is_empty() || value.starts_with(['+', '-']) || matches!(value, "!" | "^" | "$") {
611 return Ok(None);
612 }
613 value.parse::<u32>().map(Some).map_err(|_| {
614 RmuxError::invalid_target(value, "window index must fit in an unsigned integer")
615 })
616}
617
618#[cfg(test)]
619#[path = "target_find/session_recency_tests.rs"]
620mod session_recency_tests;
621#[cfg(test)]
622#[path = "target_find/tests.rs"]
623mod tests;