1use 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#[derive(Debug, Default, Clone)]
14pub struct ReadOnlyLock<T>(Arc<RwLock<T>>);
15
16impl<T> ReadOnlyLock<T> {
17 pub fn new(inner: Arc<RwLock<T>>) -> Self {
19 Self(inner)
20 }
21
22 pub fn read(&self) -> LockResult<RwLockReadGuard<'_, T>> {
24 self.0.read()
25 }
26}
27
28pub(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
85pub(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#[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}