Skip to main content

smux/
util.rs

1use std::ffi::OsStr;
2use std::path::{Path, PathBuf};
3
4use anyhow::{Context, Result, bail};
5
6pub fn command_available(command: &str) -> bool {
7    which::which(command).is_ok()
8}
9
10pub fn inside_tmux() -> bool {
11    std::env::var_os("TMUX").is_some()
12}
13
14pub fn normalize_path(path: &Path) -> Result<PathBuf> {
15    let expanded = expand_tilde(path);
16    expanded
17        .canonicalize()
18        .with_context(|| format!("failed to resolve path {}", expanded.display()))
19}
20
21pub fn expand_and_normalize_path(path: &Path) -> Result<PathBuf> {
22    let expanded = expand_tilde(path);
23    expanded
24        .canonicalize()
25        .with_context(|| format!("failed to resolve path {}", expanded.display()))
26}
27
28pub fn expand_and_absolutize_path(path: &Path) -> Result<PathBuf> {
29    let expanded = expand_tilde(path);
30
31    if expanded.exists() {
32        return expanded
33            .canonicalize()
34            .with_context(|| format!("failed to resolve path {}", expanded.display()));
35    }
36
37    if expanded.is_absolute() {
38        Ok(expanded)
39    } else {
40        let current_dir =
41            std::env::current_dir().context("failed to resolve current working directory")?;
42        Ok(current_dir.join(expanded))
43    }
44}
45
46pub fn session_name_from_path(path: &Path) -> Result<String> {
47    let basename = path
48        .file_name()
49        .and_then(OsStr::to_str)
50        .context("path does not have a valid terminal directory name")?;
51
52    let sanitized = sanitize_session_name(basename);
53
54    if sanitized.is_empty() {
55        bail!(
56            "could not derive a valid tmux session name from {}",
57            path.display()
58        );
59    }
60
61    Ok(sanitized)
62}
63
64pub fn validated_session_name(value: &str) -> Result<String> {
65    let sanitized = sanitize_session_name(value);
66
67    if sanitized.is_empty() {
68        bail!("session name resolved to an empty value");
69    }
70
71    Ok(sanitized)
72}
73
74pub fn sanitize_session_name(value: &str) -> String {
75    value
76        .trim()
77        .chars()
78        .map(|character| match character {
79            ' ' | ':' | '.' | '\t' | '\n' | '\r' => '_',
80            character if is_tmux_safe(character) => character,
81            _ => '_',
82        })
83        .collect::<String>()
84        .trim_matches('_')
85        .to_owned()
86}
87
88pub fn path_to_string(path: &Path) -> Result<String> {
89    path.to_str()
90        .map(ToOwned::to_owned)
91        .context("path was not valid utf-8")
92}
93
94fn expand_tilde(path: &Path) -> PathBuf {
95    let text = path.to_string_lossy();
96    if !text.starts_with("~/") && text != "~" {
97        return path.to_path_buf();
98    }
99
100    let Some(home) = std::env::var_os("HOME") else {
101        return path.to_path_buf();
102    };
103
104    if text == "~" {
105        return PathBuf::from(home);
106    }
107
108    let suffix = text.trim_start_matches("~/");
109    PathBuf::from(home).join(suffix)
110}
111
112fn is_tmux_safe(character: char) -> bool {
113    character.is_ascii_alphanumeric() || matches!(character, '_' | '-')
114}
115
116#[cfg(test)]
117mod tests {
118    use super::{expand_tilde, sanitize_session_name, validated_session_name};
119    use std::path::Path;
120
121    #[test]
122    fn expands_tilde_paths() {
123        let path = expand_tilde(Path::new("~/code"));
124        assert!(path.is_absolute());
125    }
126
127    #[test]
128    fn strips_invalid_session_characters() {
129        assert_eq!(sanitize_session_name(" hello/world "), "hello_world");
130    }
131
132    #[test]
133    fn rejects_empty_session_names() {
134        assert!(validated_session_name("...").is_err());
135    }
136}