use crate::utils::exec_cmd; use clap::crate_version; use std::fs; use std::path::PathBuf; #[cfg(feature = "http")] const GIT_IO_BASE_URL: &str = "https://git.io/"; pub fn create() { let os_info = os_info::get(); let environment = Environment { os_type: os_info.os_type(), os_version: os_info.version().to_owned(), shell_info: get_shell_info(), terminal_info: get_terminal_info(), starship_config: get_starship_config(), }; let link = make_github_issue_link(crate_version!(), environment); let short_link = shorten_link(&link); if open::that(&link) .map(|status| status.success()) .unwrap_or(false) { println!("Take a look at your browser. A GitHub issue has been populated with your configuration."); println!("If your browser has failed to open, please click this link:\n"); } else { println!("Click this link to create a GitHub issue populated with your configuration:\n"); } println!(" {}", short_link.unwrap_or(link)); } #[cfg(feature = "http")] fn shorten_link(link: &str) -> Option { attohttpc::post(&format!("{}{}", GIT_IO_BASE_URL, "create")) .form(&[("url", link)]) .ok() .and_then(|r| r.send().ok()) .and_then(|r| r.text().ok()) .map(|slug| format!("{}{}", GIT_IO_BASE_URL, slug)) } #[cfg(not(feature = "http"))] fn shorten_link(_url: &str) -> Option { None } const UNKNOWN_SHELL: &str = ""; const UNKNOWN_TERMINAL: &str = ""; const UNKNOWN_VERSION: &str = ""; const UNKNOWN_CONFIG: &str = ""; const GITHUB_CHAR_LIMIT: usize = 8100; // Magic number accepted by Github struct Environment { os_type: os_info::Type, os_version: os_info::Version, shell_info: ShellInfo, terminal_info: TerminalInfo, starship_config: String, } fn make_github_issue_link(starship_version: &str, environment: Environment) -> String { let body = urlencoding::encode(&format!("#### Current Behavior #### Expected Behavior #### Additional context/Screenshots #### Possible Solution #### Environment - Starship version: {starship_version} - {shell_name} version: {shell_version} - Operating system: {os_name} {os_version} - Terminal emulator: {terminal_name} {terminal_version} #### Relevant Shell Configuration ```bash {shell_config} ``` #### Starship Configuration ```toml {starship_config} ```", starship_version = starship_version, shell_name = environment.shell_info.name, shell_version = environment.shell_info.version, terminal_name = environment.terminal_info.name, terminal_version = environment.terminal_info.version, os_name = environment.os_type, os_version = environment.os_version, shell_config = environment.shell_info.config, starship_config = environment.starship_config, )) .replace("%20", "+"); format!( "https://github.com/starship/starship/issues/new?template={}&body={}", urlencoding::encode("Bug_report.md"), body ) .chars() .take(GITHUB_CHAR_LIMIT) .collect() } #[derive(Debug)] struct ShellInfo { name: String, version: String, config: String, } fn get_shell_info() -> ShellInfo { let shell = std::env::var("STARSHIP_SHELL"); if shell.is_err() { return ShellInfo { name: UNKNOWN_SHELL.to_string(), version: UNKNOWN_VERSION.to_string(), config: UNKNOWN_CONFIG.to_string(), }; } let shell = shell.unwrap(); let version = exec_cmd(&shell, &["--version"]) .map(|output| output.stdout.trim().to_string()) .unwrap_or_else(|| UNKNOWN_VERSION.to_string()); let config = get_config_path(&shell) .and_then(|config_path| fs::read_to_string(config_path).ok()) .map(|config| config.trim().to_string()) .unwrap_or_else(|| UNKNOWN_CONFIG.to_string()); ShellInfo { name: shell, version, config, } } #[derive(Debug)] struct TerminalInfo { name: String, version: String, } fn get_terminal_info() -> TerminalInfo { let terminal = std::env::var("TERM_PROGRAM") .or_else(|_| std::env::var("LC_TERMINAL")) .unwrap_or_else(|_| UNKNOWN_TERMINAL.to_string()); let version = std::env::var("TERM_PROGRAM_VERSION") .or_else(|_| std::env::var("LC_TERMINAL_VERSION")) .unwrap_or_else(|_| UNKNOWN_VERSION.to_string()); TerminalInfo { name: terminal, version, } } fn get_config_path(shell: &str) -> Option { dirs_next::home_dir().and_then(|home_dir| { match shell { "bash" => Some(".bashrc"), "fish" => Some(".config/fish/config.fish"), "ion" => Some(".config/ion/initrc"), "powershell" => { if cfg!(windows) { Some("Documents/PowerShell/Microsoft.PowerShell_profile.ps1") } else { Some(".config/powershell/Microsoft.PowerShell_profile.ps1") } } "zsh" => Some(".zshrc"), _ => None, } .map(|path| home_dir.join(path)) }) } fn get_starship_config() -> String { std::env::var("STARSHIP_CONFIG") .map(PathBuf::from) .ok() .or_else(|| { dirs_next::home_dir().map(|mut home_dir| { home_dir.push(".config/starship.toml"); home_dir }) }) .and_then(|config_path| fs::read_to_string(config_path).ok()) .unwrap_or_else(|| UNKNOWN_CONFIG.to_string()) } #[cfg(test)] mod tests { use super::*; use std::env; #[test] fn test_make_github_link() { let starship_version = "0.1.2"; let environment = Environment { os_type: os_info::Type::Linux, os_version: os_info::Version::Semantic(1, 2, 3), shell_info: ShellInfo { name: "test_shell".to_string(), version: "2.3.4".to_string(), config: "No config".to_string(), }, terminal_info: TerminalInfo { name: "test_terminal".to_string(), version: "5.6.7".to_string(), }, starship_config: "No Starship config".to_string(), }; let link = make_github_issue_link(starship_version, environment); assert!(link.contains(starship_version)); assert!(link.contains("Linux")); assert!(link.contains("1.2.3")); assert!(link.contains("test_shell")); assert!(link.contains("2.3.4")); assert!(link.contains("No+config")); assert!(link.contains("No+Starship+config")); } #[test] fn test_get_shell_info() { env::remove_var("STARSHIP_SHELL"); let unknown_shell = get_shell_info(); assert_eq!(UNKNOWN_SHELL, &unknown_shell.name); env::set_var("STARSHIP_SHELL", "fish"); let fish_shell = get_shell_info(); assert_eq!("fish", &fish_shell.name); } #[test] #[cfg(not(windows))] fn test_get_config_path() { let config_path = get_config_path("bash"); assert_eq!( dirs_next::home_dir().unwrap().join(".bashrc"), config_path.unwrap() ); } }