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