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