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