mj_controller/controller/
path_completion.rs1use std::path::Path;
9
10use anyhow::{Context, Result};
11use mj_core::path_completion::{
12 CompletionHost, CompletionKind, MAX_CANDIDATES, PathCompletion, common_insert,
13 local_completions, ssh_completions,
14};
15
16use super::Controller;
17use super::cache_host::CacheHost;
18use crate::targets::CommandExecutor;
19
20impl Controller {
21 pub fn complete_path(
24 &self,
25 host: &CompletionHost,
26 prefix: &str,
27 kind: CompletionKind,
28 executor: &impl CommandExecutor,
29 ) -> Result<PathCompletion> {
30 if prefix.is_empty() {
31 return Ok(PathCompletion::default());
32 }
33 let host = match host {
34 CompletionHost::Local => CacheHost::Local,
35 CompletionHost::Target(target_id) => {
36 let target = self
37 .config
38 .targets
39 .get(target_id)
40 .with_context(|| format!("unknown target template {target_id:?}"))?;
41 CacheHost::for_path_target(target)?
42 }
43 CompletionHost::Machine(machine) => CacheHost::for_path_machine(machine)?,
44 };
45 let home = if mj_core::path_input::needs_home(Path::new(prefix))? {
46 Some(host.home(executor)?)
47 } else {
48 None
49 };
50 let expanded = mj_core::path_input::expand_home(Path::new(prefix), home.as_deref())?;
51 let mut lookup = expanded.to_string_lossy().into_owned();
52 if prefix.ends_with('/') && !lookup.ends_with('/') {
54 lookup.push('/');
55 }
56 let mut candidates = match &host {
57 CacheHost::Local => local_completions(&lookup, kind),
58 CacheHost::Ssh(ssh) => ssh_completions(ssh, &lookup, kind, executor)?,
59 };
60 let truncated = candidates.len() > MAX_CANDIDATES;
61 candidates.truncate(MAX_CANDIDATES);
62 let candidates = candidates
63 .into_iter()
64 .map(|candidate| fold_home(candidate, home.as_deref()))
65 .collect::<Result<Vec<_>>>()?;
66 let insert = common_insert(prefix, &candidates);
67 Ok(PathCompletion {
68 candidates,
69 insert,
70 truncated,
71 })
72 }
73}
74
75fn fold_home(candidate: String, home: Option<&Path>) -> Result<String> {
78 let Some(home) = home else {
79 return Ok(candidate);
80 };
81 let suffix = Path::new(&candidate)
82 .strip_prefix(home)
83 .context("Completed path is outside the requested home")?;
84 let mut value = Path::new("~").join(suffix).to_string_lossy().into_owned();
85 if candidate.ends_with('/') && !value.ends_with('/') {
86 value.push('/');
87 }
88 Ok(value)
89}