1#![feature(exit_status_error)]
16
17use core::str::FromStr;
18use std::path::Component;
19use std::path::Path;
20use std::path::PathBuf;
21use std::process::Command;
22use std::sync::Arc;
23
24use color_eyre::eyre::bail;
25use color_eyre::eyre::eyre;
26use url::Url;
27use ytil_editor::Editor;
28use ytil_hx::HxCursorPosition;
29use ytil_hx::HxStatusLine;
30use ytil_sys::cli::Args;
31use ytil_wezterm::WeztermPane;
32use ytil_wezterm::get_sibling_pane_with_titles;
33
34fn build_hx_cursor_absolute_file_path(
39 hx_cursor_file_path: &Path,
40 hx_pane: &WeztermPane,
41) -> color_eyre::Result<PathBuf> {
42 if let Ok(hx_cursor_file_path) = hx_cursor_file_path.strip_prefix("~") {
43 return ytil_sys::dir::build_home_path(&[hx_cursor_file_path]);
44 }
45
46 let mut components = hx_pane.cwd.components();
47 components.next();
48 components.next();
49
50 Ok(std::iter::once(Component::RootDir)
51 .chain(components)
52 .chain(hx_cursor_file_path.components())
53 .collect())
54}
55
56fn build_github_link<'a>(
61 github_repo_url: &'a Url,
62 git_current_branch: &'a str,
63 file_path: &'a Path,
64 hx_cursor_position: &'a HxCursorPosition,
65) -> color_eyre::Result<Url> {
66 let mut file_path_parts = vec![];
67 for component in file_path.components() {
68 file_path_parts.push(
69 component
70 .as_os_str()
71 .to_str()
72 .ok_or_else(|| eyre!("path component invalid utf-8 | component={component:#?}"))?,
73 );
74 }
75
76 let segments = [&["tree", git_current_branch], file_path_parts.as_slice()].concat();
77 let mut github_link = github_repo_url.clone();
78 github_link
79 .path_segments_mut()
80 .map_err(|()| eyre!("cannot extend url with segments | url={github_repo_url} segments={segments:#?}"))?
81 .extend(&segments);
82 github_link.set_fragment(Some(&format!(
83 "L{}C{}",
84 hx_cursor_position.line, hx_cursor_position.column
85 )));
86
87 Ok(github_link)
88}
89
90fn main() -> color_eyre::Result<()> {
92 color_eyre::install()?;
93
94 let args = ytil_sys::cli::get();
95 if args.has_help() {
96 println!("{}", include_str!("../help.txt"));
97 return Ok(());
98 }
99
100 let hx_pane = get_sibling_pane_with_titles(
101 &ytil_wezterm::get_all_panes(&[])?,
102 ytil_wezterm::get_current_pane_id()?,
103 Editor::Hx.pane_titles(),
104 )?;
105
106 let wezterm_pane_text = String::from_utf8(
107 Command::new("wezterm")
108 .args(["cli", "get-text", "--pane-id", &hx_pane.pane_id.to_string()])
109 .output()?
110 .stdout,
111 )?;
112
113 let hx_status_line = HxStatusLine::from_str(wezterm_pane_text.lines().nth_back(1).ok_or_else(|| {
114 eyre!(
115 "missing hx status line | pane_id={} text={wezterm_pane_text:#?}",
116 hx_pane.pane_id
117 )
118 })?)?;
119
120 let git_repo_root_path = Arc::new(ytil_git::repo::get_root(&ytil_git::repo::discover(
121 &hx_status_line.file_path,
122 )?));
123
124 let get_git_current_branch =
125 std::thread::spawn(move || -> color_eyre::Result<String> { ytil_git::branch::get_current() });
126
127 let git_repo_root_path_clone = git_repo_root_path.clone();
128 let get_github_repo_url = std::thread::spawn(move || -> color_eyre::Result<Url> {
129 match &ytil_gh::get_repo_urls(&git_repo_root_path_clone)?.as_slice() {
130 &[] => bail!("missing GitHub repo URL | repo_path={git_repo_root_path_clone:#?}"),
131 &[one] => Ok(one.clone()),
132 multi => {
133 bail!("multiple GitHub repo URLs | URLs={multi:#?} repo_path={git_repo_root_path_clone:#?}")
134 }
135 }
136 });
137
138 let hx_cursor_absolute_file_path = build_hx_cursor_absolute_file_path(&hx_status_line.file_path, &hx_pane)?;
141
142 let github_link = build_github_link(
143 &ytil_sys::join(get_github_repo_url)?,
144 &ytil_sys::join(get_git_current_branch)?,
145 hx_cursor_absolute_file_path.strip_prefix(git_repo_root_path.as_ref())?,
146 &hx_status_line.position,
147 )?;
148
149 ytil_sys::file::cp_to_system_clipboard(&mut github_link.as_str().as_bytes())?;
150
151 Ok(())
152}
153
154#[cfg(test)]
155mod tests {
156 use fake::Fake;
157 use fake::Faker;
158
159 use super::*;
160
161 #[test]
162 fn build_hx_cursor_absolute_file_path_works_as_expected_with_file_path_as_relative_to_home_dir() {
163 temp_env::with_vars([("HOME", Some("/Users/Foo"))], || {
165 let hx_status_line = HxStatusLine {
166 file_path: Path::new("~/src/bar/baz.rs").into(),
167 ..Faker.fake()
168 };
169 let hx_pane = WeztermPane {
170 cwd: Path::new("file://hostname/Users/Foo/dev").into(),
171 ..Faker.fake()
172 };
173
174 let result = build_hx_cursor_absolute_file_path(&hx_status_line.file_path, &hx_pane);
176
177 let expected = Path::new("/Users/Foo/src/bar/baz.rs").to_path_buf();
179 assert_eq!(result.unwrap(), expected);
180 });
181 }
182
183 #[test]
184 fn build_hx_cursor_absolute_file_path_works_as_expected_with_file_path_as_relative_to_hx_root() {
185 let hx_status_line = HxStatusLine {
187 file_path: Path::new("src/bar/baz.rs").into(),
188 ..Faker.fake()
189 };
190 let hx_pane = WeztermPane {
191 cwd: Path::new("file://hostname/Users/Foo/dev").into(),
192 ..Faker.fake()
193 };
194
195 let result = build_hx_cursor_absolute_file_path(&hx_status_line.file_path, &hx_pane).unwrap();
197
198 let expected = Path::new("/Users/Foo/dev/src/bar/baz.rs").to_path_buf();
200 assert_eq!(expected, result);
201 }
202
203 #[test]
204 fn build_hx_cursor_absolute_file_path_works_as_expected_with_file_path_as_absolute() {
205 let hx_status_line = HxStatusLine {
207 file_path: Path::new("/Users/Foo/dev/src/bar/baz.rs").into(),
208 ..Faker.fake()
209 };
210 let hx_pane = WeztermPane {
211 cwd: Path::new("file://hostname/Users/Foo/dev").into(),
212 ..Faker.fake()
213 };
214
215 let result = build_hx_cursor_absolute_file_path(&hx_status_line.file_path, &hx_pane).unwrap();
217
218 let expected = Path::new("/Users/Foo/dev/src/bar/baz.rs").to_path_buf();
220 assert_eq!(expected, result);
221 }
222}