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, TargetFindContext, TargetFindFlags, TargetFindType,
40 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 Err(RmuxError::Server(
70 "target form {marked} is recognized but deferred until marked-pane state exists"
71 .to_owned(),
72 ));
73 }
74
75 let mut parts = TargetParts::parse(raw, find_type);
76 let mut flags = flags;
77 parts.apply_exact_prefixes(&mut flags);
78 if flags.contains(TargetFindFlags::WINDOW_INDEX)
79 && parts.window_only
80 && parts.window == Some("")
81 && parts.pane.is_none()
82 {
83 return self.resolve_empty_window_index_target(
84 raw,
85 parts.session,
86 find_type,
87 flags,
88 context,
89 );
90 }
91 parts.drop_empty();
92 parts.map_tokens();
93 if flags.contains(TargetFindFlags::WINDOW_INDEX) && parts.pane.is_some() {
94 return Err(RmuxError::invalid_target(raw, "can't specify pane here"));
95 }
96
97 self.resolve_parts(raw, parts, find_type, flags, context)
98 }
99
100 fn resolve_parts(
101 &self,
102 raw: &str,
103 parts: TargetParts<'_>,
104 find_type: TargetFindType,
105 flags: TargetFindFlags,
106 context: &TargetFindContext,
107 ) -> Result<Target, RmuxError> {
108 if let Some(session) = parts.session {
109 let session_name = self.resolve_session_part(session, flags)?;
110 if parts.window.is_none() && parts.pane.is_none() {
111 return self.target_for_session_current(find_type, session_name);
112 }
113 if let Some(window) = parts.window {
114 let window = self.resolve_window_in_session(raw, &session_name, window, flags)?;
115 if let Some(pane) = parts.pane {
116 let pane = self.resolve_pane_in_window(
117 raw,
118 &window.session_name,
119 window.window_index,
120 pane,
121 )?;
122 return Ok(target_for_type(find_type, pane));
123 }
124 return self.target_for_window_current(find_type, window);
125 }
126
127 let pane = self.resolve_pane_in_session(
128 raw,
129 &session_name,
130 parts.pane.expect("pane branch requires pane"),
131 )?;
132 return Ok(target_for_type(find_type, pane));
133 }
134
135 if let Some(window) = parts.window {
136 let window =
137 self.resolve_window_from_context(raw, window, parts.window_only, flags, context)?;
138 if let Some(pane) = parts.pane {
139 let pane = self.resolve_pane_in_window(
140 raw,
141 &window.session_name,
142 window.window_index,
143 pane,
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 let window_id = crate::WindowId::new(parse_required_prefixed_id(value, '@')?);
260 let Some(window_index) = session
261 .windows()
262 .iter()
263 .find_map(|(index, window)| (window.id() == window_id).then_some(*index))
264 else {
265 return Err(target_not_found(value, "window"));
266 };
267 return self.parts_for_window(session_name, window_index);
268 }
269
270 if !flags.contains(TargetFindFlags::EXACT_WINDOW) {
271 if let Some(offset) = parse_offset(value)? {
272 let window_index = if flags.contains(TargetFindFlags::WINDOW_INDEX) {
273 offset_index(session.active_window_index(), offset)
274 .ok_or_else(|| target_not_found(value, "window"))?
275 } else {
276 cyclic_window_offset(session, offset)
277 .ok_or_else(|| target_not_found(value, "window"))?
278 };
279 return Ok(ResolvedParts::window(session_name.clone(), window_index));
280 }
281
282 if let Some(window_index) = special_window_index(session, value) {
283 return self.parts_for_window(session_name, window_index);
284 }
285 }
286
287 if !value.starts_with(['+', '-']) {
288 if let Ok(window_index) = value.parse::<u32>() {
289 if session.window_at(window_index).is_some() {
290 return self.parts_for_window(session_name, window_index);
291 }
292 if flags.contains(TargetFindFlags::WINDOW_INDEX) {
293 return Ok(ResolvedParts::window(session_name.clone(), window_index));
294 }
295 }
296 }
297
298 if let Some(window_index) = unique_window_name_match(session, value, MatchMode::Exact)? {
299 return self.parts_for_window(session_name, window_index);
300 }
301
302 if flags.contains(TargetFindFlags::EXACT_WINDOW) {
303 return Err(target_not_found(value, "window"));
304 }
305
306 if let Some(window_index) = unique_window_name_match(session, value, MatchMode::Prefix)? {
307 return self.parts_for_window(session_name, window_index);
308 }
309 if let Some(window_index) = unique_window_name_match(session, value, MatchMode::Pattern)? {
310 return self.parts_for_window(session_name, window_index);
311 }
312
313 Err(RmuxError::invalid_target(
314 raw,
315 format!("can't find window: {value}"),
316 ))
317 }
318
319 fn resolve_window_id(
320 &self,
321 raw: &str,
322 value: &str,
323 context: &TargetFindContext,
324 ) -> Result<ResolvedParts, RmuxError> {
325 let window_id = crate::WindowId::new(parse_required_prefixed_id(value, '@')?);
326 if let Ok(current) = self.current_parts(context) {
327 if let Some(window) = self
328 .session(¤t.session_name)
329 .and_then(|session| session.window_at(current.window_index))
330 {
331 if window.id() == window_id {
332 return Ok(ResolvedParts::window(
333 current.session_name,
334 current.window_index,
335 ));
336 }
337 }
338 }
339
340 let mut matches = self
341 .iter()
342 .flat_map(|(session_name, session)| {
343 session
344 .windows()
345 .iter()
346 .filter(move |(_, window)| window.id() == window_id)
347 .map(move |(window_index, _)| {
348 ResolvedParts::window(session_name.clone(), *window_index)
349 })
350 })
351 .collect::<Vec<_>>();
352 matches.sort_by(|left, right| {
353 left.session_name
354 .as_str()
355 .cmp(right.session_name.as_str())
356 .then(left.window_index.cmp(&right.window_index))
357 });
358 unique_match(matches, raw, "window")?.ok_or_else(|| target_not_found(value, "window"))
359 }
360
361 fn resolve_pane_from_context(
362 &self,
363 raw: &str,
364 value: &str,
365 only_pane: bool,
366 flags: TargetFindFlags,
367 context: &TargetFindContext,
368 ) -> Result<ResolvedParts, RmuxError> {
369 if value.starts_with('%') {
370 return self.resolve_pane_id(raw, value, context);
371 }
372
373 let current = match self.current_parts(context) {
374 Ok(current) => current,
375 Err(error) if !only_pane => {
376 return self
377 .resolve_window_from_context(raw, value, false, flags, context)
378 .and_then(|window| {
379 self.target_for_window_current(TargetFindType::Pane, window)
380 .and_then(|target| {
381 if let Target::Pane(target) = target {
382 Ok(ResolvedParts::pane(
383 target.session_name().clone(),
384 target.window_index(),
385 target.pane_index(),
386 ))
387 } else {
388 Err(RmuxError::Server(
389 "pane fallback produced non-pane target".to_owned(),
390 ))
391 }
392 })
393 })
394 .map_err(|_| error);
395 }
396 Err(error) => return Err(error),
397 };
398 match self.resolve_pane_in_window(raw, ¤t.session_name, current.window_index, value) {
399 Ok(pane) => Ok(pane),
400 Err(error) if !only_pane => {
401 match self.resolve_window_from_context(raw, value, false, flags, context) {
402 Ok(window) => self
403 .target_for_window_current(TargetFindType::Pane, window)
404 .and_then(|target| {
405 if let Target::Pane(target) = target {
406 Ok(ResolvedParts::pane(
407 target.session_name().clone(),
408 target.window_index(),
409 target.pane_index(),
410 ))
411 } else {
412 Err(RmuxError::Server(
413 "pane fallback produced non-pane target".to_owned(),
414 ))
415 }
416 }),
417 Err(_) => Err(error),
418 }
419 }
420 Err(error) => Err(error),
421 }
422 }
423
424 fn resolve_pane_in_session(
425 &self,
426 raw: &str,
427 session_name: &SessionName,
428 value: &str,
429 ) -> Result<ResolvedParts, RmuxError> {
430 if value.starts_with('%') {
431 let pane_id = crate::PaneId::new(parse_required_prefixed_id(value, '%')?);
432 let session = self
433 .session(session_name)
434 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?;
435 let Some((window_index, pane_index)) = find_pane_id_in_session(session, pane_id) else {
436 return Err(target_not_found(value, "pane"));
437 };
438 return Ok(ResolvedParts::pane(
439 session_name.clone(),
440 window_index,
441 pane_index,
442 ));
443 }
444
445 let window_index = self
446 .session(session_name)
447 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?
448 .active_window_index();
449 self.resolve_pane_in_window(raw, session_name, window_index, value)
450 }
451
452 fn resolve_pane_in_window(
453 &self,
454 raw: &str,
455 session_name: &SessionName,
456 window_index: u32,
457 value: &str,
458 ) -> Result<ResolvedParts, RmuxError> {
459 let session = self
460 .session(session_name)
461 .ok_or_else(|| target_not_found(session_name.as_str(), "session"))?;
462 let window = session
463 .window_at(window_index)
464 .ok_or_else(|| target_not_found(&format!("{session_name}:{window_index}"), "window"))?;
465
466 if value.starts_with('%') {
467 let pane_id = crate::PaneId::new(parse_required_prefixed_id(value, '%')?);
468 let Some(pane) = window.panes().iter().find(|pane| pane.id() == pane_id) else {
469 return Err(target_not_found(value, "pane"));
470 };
471 return Ok(ResolvedParts::pane(
472 session_name.clone(),
473 window_index,
474 pane.index(),
475 ));
476 }
477
478 if value == "!" {
479 let pane_index = window
480 .last_pane_index()
481 .ok_or_else(|| target_not_found(value, "pane"))?;
482 return Ok(ResolvedParts::pane(
483 session_name.clone(),
484 window_index,
485 pane_index,
486 ));
487 }
488
489 if let Some(pane_index) = directional_pane(window, value) {
490 return Ok(ResolvedParts::pane(
491 session_name.clone(),
492 window_index,
493 pane_index,
494 ));
495 }
496
497 if let Some(offset) = parse_offset(value)? {
498 let pane_index = cyclic_pane_offset(window, offset)
499 .ok_or_else(|| target_not_found(value, "pane"))?;
500 return Ok(ResolvedParts::pane(
501 session_name.clone(),
502 window_index,
503 pane_index,
504 ));
505 }
506
507 if let Ok(pane_index) = value.parse::<u32>() {
508 if window.pane(pane_index).is_some() {
509 return Ok(ResolvedParts::pane(
510 session_name.clone(),
511 window_index,
512 pane_index,
513 ));
514 }
515 }
516
517 if let Some(pane_index) = pane_description(window, value) {
518 return Ok(ResolvedParts::pane(
519 session_name.clone(),
520 window_index,
521 pane_index,
522 ));
523 }
524
525 Err(RmuxError::invalid_target(
526 raw,
527 format!("can't find pane: {value}"),
528 ))
529 }
530
531 fn resolve_pane_id(
532 &self,
533 raw: &str,
534 value: &str,
535 context: &TargetFindContext,
536 ) -> Result<ResolvedParts, RmuxError> {
537 let pane_id = crate::PaneId::new(parse_required_prefixed_id(value, '%')?);
538 if let Ok(current) = self.current_parts(context) {
539 if let Some(pane) = self
540 .session(¤t.session_name)
541 .and_then(|session| session.window_at(current.window_index))
542 .and_then(|window| window.pane(current.pane_index))
543 {
544 if pane.id() == pane_id {
545 return Ok(current);
546 }
547 }
548 }
549
550 let mut matches = self
551 .iter()
552 .flat_map(|(session_name, session)| {
553 session
554 .windows()
555 .iter()
556 .flat_map(move |(window_index, window)| {
557 window
558 .panes()
559 .iter()
560 .filter(move |pane| pane.id() == pane_id)
561 .map(move |pane| {
562 ResolvedParts::pane(
563 session_name.clone(),
564 *window_index,
565 pane.index(),
566 )
567 })
568 })
569 })
570 .collect::<Vec<_>>();
571 matches.sort_by(|left, right| {
572 left.session_name
573 .as_str()
574 .cmp(right.session_name.as_str())
575 .then(left.window_index.cmp(&right.window_index))
576 .then(left.pane_index.cmp(&right.pane_index))
577 });
578 unique_match(matches, raw, "pane")?.ok_or_else(|| target_not_found(value, "pane"))
579 }
580}
581
582#[cfg(test)]
583#[path = "target_find/tests.rs"]
584mod tests;