Skip to main content

rmux_core/
target_find.rs

1//! tmux-style unresolved target lookup over live in-memory session state.
2//!
3//! This module intentionally sits above the exact protocol target structs in
4//! `rmux-proto`: it accepts raw command target text and resolves it to the
5//! existing `Target`, `WindowTarget`, and `PaneTarget` output representation.
6//! Frozen source anchors: `/opt/rmux/reference/tmux` at commit
7//! `31d77e29b6c9fbb07d032018da78db3a8a38d979`, especially
8//! `cmd-find.c:247`, `cmd-find.c:347`, `cmd-find.c:563`, and
9//! `cmd-find.c:922`.
10
11use 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    /// Resolves an unresolved command target to the existing typed target representation.
45    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, &current.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(&current.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, &current.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(&current.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;