ad_editor/
util.rs

1//! Utility functions
2use crate::{config::config_path, editor::built_in_commands, mode::keybindings};
3use std::{
4    fs,
5    os::unix::fs::PermissionsExt,
6    path::Path,
7    sync::{Arc, LockResult, RwLock, RwLockReadGuard},
8};
9use tracing::warn;
10
11/// A wrapper around an `Arc<RwLock<T>>` so that the owner is only
12/// permitted read access to the underlying value.
13#[derive(Debug, Default, Clone)]
14pub struct ReadOnlyLock<T>(Arc<RwLock<T>>);
15
16impl<T> ReadOnlyLock<T> {
17    /// Construct a new ReadOnlyLock wrapping an inner `Arc<RwLock<T>>`
18    pub fn new(inner: Arc<RwLock<T>>) -> Self {
19        Self(inner)
20    }
21
22    /// Obtain a read guard from the underlying `RwLock`
23    pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
24        self.0.read()
25    }
26}
27
28/// Pull in data from the ad crate itself to auto-generate the docs on the functionality
29/// available in the editor.
30pub(crate) fn gen_help_docs() -> String {
31    let help_template = include_str!("../data/help-template.txt");
32
33    help_template
34        .replace("{{KEY_BINDINGS}}", &keybindings_section())
35        .replace("{{BUILT_IN_COMMANDS}}", &commands_section())
36        .replace("{{CONFIG_PATH}}", &config_path())
37}
38
39fn keybindings_section() -> String {
40    let raw = keybindings();
41    let mut sections = Vec::with_capacity(raw.len());
42
43    for (mode, bindings) in raw.into_iter() {
44        let w_max = bindings.iter().map(|(s, _)| s.len()).max().unwrap();
45        let mut section = format!("{mode} mode\n");
46
47        for (keys, desc) in bindings.into_iter() {
48            section.push_str(&format!("  {:width$} -- {desc}\n", keys, width = w_max));
49        }
50
51        sections.push(section);
52    }
53
54    sections.join("\n\n")
55}
56
57fn commands_section() -> String {
58    let commands = built_in_commands();
59    let mut buf = Vec::with_capacity(commands.len());
60
61    for (cmds, desc) in commands.into_iter() {
62        buf.push((cmds.join(" | "), desc));
63    }
64
65    let w_max = buf.iter().map(|(s, _)| s.len()).max().unwrap();
66    let mut s = String::new();
67
68    for (cmds, desc) in buf.into_iter() {
69        s.push_str(&format!("{:width$} -- {desc}\n", cmds, width = w_max));
70    }
71
72    s
73}
74
75pub(crate) fn normalize_line_endings(mut s: String) -> String {
76    if !s.contains('\r') {
77        return s;
78    }
79
80    warn!("normalizing \\r characters to \\n");
81    s = s.replace("\r\n", "\n");
82    s.replace("\r", "\n")
83}
84
85/// Locate the first parent directory containing a target file
86pub(crate) fn parent_dir_containing<'a>(initial: &'a Path, target: &str) -> Option<&'a Path> {
87    initial
88        .ancestors()
89        .find(|&p| p.is_dir() && p.join(target).exists())
90}
91
92/// Check whether or not a given command can be found as an executable within the provided set of path directories
93#[allow(dead_code)]
94pub(crate) fn exists_on_path_as_executable(cmd: &str, cwd: &Path, path_str: &str) -> bool {
95    let cwd_candidate = cwd.join(cmd);
96    let candidates = path_str.split(':').map(|dir| Path::new(dir).join(cmd));
97
98    for candidate in std::iter::once(cwd_candidate).chain(candidates) {
99        if let Ok(meta) = fs::metadata(candidate)
100            && meta.is_file()
101            && meta.permissions().mode() & 0o111 != 0
102        {
103            return true;
104        }
105    }
106
107    false
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113    use simple_test_case::test_case;
114    use std::{env, path::PathBuf};
115
116    #[test_case("cat", true; "cat exists")]
117    #[test_case("dog", false; "dog does not exist")]
118    #[test]
119    fn executable_checking_works(cmd: &str, expected: bool) {
120        let path = env::var("PATH").unwrap();
121        let exists = exists_on_path_as_executable(cmd, &PathBuf::from("/tmp"), &path);
122
123        assert_eq!(exists, expected);
124    }
125}