what_the_path/
shell.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;

use crate::dirs::get_home_dir;

#[derive(Debug)]
/// Represents different types of Unix shells supported by this library.
///
/// This enum provides variants for common Unix shells (POSIX, Zsh, Bash, Fish)
/// along with their specific configuration handling.
///
/// # Examples
///
/// ```
/// use what_the_path::Shell;
///
/// // Detect current shell
/// if let Some(shell) = Shell::detect() {
///     match shell {
///         Shell::Zsh(_) => println!("Using Zsh"),
///         Shell::Bash(_) => println!("Using Bash"),
///         Shell::Fish(_) => println!("Using Fish"),
///         Shell::POSIX(_) => println!("Using POSIX shell"),
///     }
/// }
/// ```
///
/// # Variants
///
/// * `POSIX` - Default POSIX-compliant shell (like sh)
/// * `Zsh` - Z shell
/// * `Bash` - Bourne Again Shell
/// * `Fish` - Friendly Interactive Shell
///
pub enum Shell {
    POSIX(POSIX),
    Zsh(Zsh),
    Bash(Bash),
    Fish(Fish),
}

impl Shell {
    /// Detects the current shell by examining the `SHELL` environment variable.
    ///
    /// This function attempts to identify the shell type based on the `SHELL` environment variable.
    /// It will return `None` on Windows systems as the `SHELL` variable is not typically used.
    ///
    /// # Returns
    /// - `Some(Shell)` containing the detected shell type if:
    ///   - Running on a non-Windows system
    ///   - The `SHELL` environment variable exists and contains a recognized shell name
    /// - `None` if:
    ///   - Running on Windows
    ///   - The `SHELL` environment variable does not exist
    ///
    /// # Shell Detection
    /// The following shells are recognized (in order):
    /// - Zsh
    /// - Bash
    /// - Fish
    /// - Any other shell is assumed to be POSIX-compliant
    pub fn detect_by_shell_var() -> Option<Shell> {
        if cfg!(windows) {
            return None;
        }

        match env::var("SHELL").ok()?.as_str() {
            shell if shell.contains("zsh") => Some(Shell::Zsh(Zsh)),
            shell if shell.contains("bash") => Some(Shell::Bash(Bash)),
            shell if shell.contains("fish") => Some(Shell::Fish(Fish)),
            _ => Some(Shell::POSIX(POSIX)),
        }
    }
}

#[derive(Debug)]
pub struct POSIX;

impl POSIX {
    pub fn does_exist() -> bool {
        true
    }
    pub fn get_rcfiles() -> Option<Vec<PathBuf>> {
        let dir = get_home_dir()?;
        Some(vec![dir.join(".profile")])
    }
}

#[derive(Debug)]
pub struct Zsh;

impl Zsh {
    pub fn does_exist() -> bool {
        matches!(env::var("SHELL"), Ok(v) if v.contains("zsh"))
            || Command::new("zsh").output().is_ok()
    }

    pub fn get_rcfiles() -> Option<Vec<PathBuf>> {
        let output = std::process::Command::new("zsh")
            .args(["-c", "echo -n $ZDOTDIR"])
            .output()
            .ok()?;

        if output.stdout.is_empty() {
            return None;
        }

        // give location
        let location = PathBuf::from(String::from_utf8(output.stdout).ok()?.trim());
        Some(vec![location.join(".zshenv")])
    }
}

#[derive(Debug)]
pub struct Bash;

impl Bash {
    pub fn does_exist() -> bool {
        matches!(env::var("SHELL"), Ok(v) if v.contains("bash"))
            || Command::new("bash").output().is_ok()
    }

    pub fn get_rcfiles() -> Option<Vec<PathBuf>> {
        let dir = get_home_dir()?;
        let rcfiles = [".bash_profile", ".bash_login", ".bashrc"]
            .iter()
            .map(|rc| dir.join(rc))
            .collect();
        Some(rcfiles)
    }
}

#[derive(Debug)]
pub struct Fish;

impl Fish {
    pub fn does_exist() -> bool {
        matches!(env::var("SHELL"), Ok(v) if v.contains("fish"))
            || Command::new("fish").output().is_ok()
    }

    /// Returns the configuration directory path for Fish shell
    ///
    /// This function attempts to locate the Fish shell's configuration directory
    /// by joining the user's home directory with the Fish config path.
    ///
    /// # Returns
    /// - `Some(Vec<PathBuf>)` containing the path to Fish's conf.d directory
    /// - `None` if the home directory cannot be determined
    ///
    /// # Important
    /// Note that this function returns a directory path (`conf.d`), not individual
    /// file paths. You'll need to enumerate the directory contents to access
    /// specific configuration files.
    ///
    /// # Example
    /// ```
    /// if let Some(paths) = get_rcfiles() {
    ///     // paths[0] points to ~/.config/fish/conf.d directory
    ///     // not to specific .fish files
    /// }
    /// ```
    pub fn get_rcfiles() -> Option<Vec<PathBuf>> {
        let mut paths = vec![];

        if let Some(path) = env::var("XDG_CONFIG_HOME").ok() {
            paths.push(PathBuf::from(path).join(".config/fish/conf.d"));
        };

        if let Some(path) = get_home_dir() {
            paths.push(path.join(".config/fish/conf.d"));
        }

        Some(paths)
    }
}

pub fn does_path_exist(path: impl AsRef<Path>) -> bool {
    matches!(env::var("PATH"), Ok(paths) if paths.contains(path.as_ref().to_str().unwrap()))
}

pub fn append_to_rcfile(rcfile: PathBuf, line: &str) -> std::io::Result<()> {
    use std::fs::OpenOptions;
    use std::io::Write;

    let mut file = OpenOptions::new().append(true).open(rcfile)?;
    writeln!(file, "{}", line)
}

pub fn remove_from_rcfile(rcfile: PathBuf, line: &str) -> std::io::Result<()> {
    let line_bytes = line.as_bytes();

    let file = std::fs::read_to_string(&rcfile)?;
    let file_bytes = file.as_bytes();

    if let Some(idx) = file_bytes
        .windows(line_bytes.len())
        .position(|w| w == line_bytes)
    {
        let mut new_bytes = file_bytes[..idx].to_vec();
        new_bytes.extend(&file_bytes[idx + line_bytes.len()..]);
        let content = String::from_utf8(new_bytes).unwrap();
        std::fs::write(&rcfile, content)?;
    }

    Ok(())
}