mod abort_signal; mod clipboard; mod command; mod crypto; mod html_to_md; mod input; mod loader; mod logs; pub mod native; mod path; mod render_prompt; mod request; mod spinner; mod variables; pub use self::abort_signal::*; pub use self::clipboard::set_text; pub use self::command::*; pub use self::crypto::*; pub use self::html_to_md::*; pub use self::input::*; pub use self::loader::*; pub use self::logs::*; pub use self::path::*; pub use self::render_prompt::render_prompt; pub use self::request::*; pub use self::spinner::*; pub use self::variables::*; use anyhow::{Context, Result}; use fancy_regex::Regex; use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2}; use is_terminal::IsTerminal; use std::borrow::Cow; use std::sync::LazyLock; use std::{env, path::PathBuf, process}; use unicode_segmentation::UnicodeSegmentation; pub static CODE_BLOCK_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?ms)```\w*(.*)```").unwrap()); pub static THINK_TAG_RE: LazyLock = LazyLock::new(|| Regex::new(r"(?s)^\s*.*?(\s*|$)").unwrap()); pub static IS_STDOUT_TERMINAL: LazyLock = LazyLock::new(|| std::io::stdout().is_terminal()); pub static NO_COLOR: LazyLock = LazyLock::new(|| { env::var("NO_COLOR") .ok() .and_then(|v| parse_bool(&v)) .unwrap_or_default() || !*IS_STDOUT_TERMINAL }); pub fn now() -> String { chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, false) } pub fn now_timestamp() -> i64 { chrono::Local::now().timestamp() } pub fn get_env_name(key: &str) -> String { format!("{}_{key}", env!("CARGO_CRATE_NAME"),).to_ascii_uppercase() } pub fn normalize_env_name(value: &str) -> String { value.replace('-', "_").to_ascii_uppercase() } pub fn parse_bool(value: &str) -> Option { match value { "1" | "true" => Some(true), "0" | "false" => Some(false), _ => None, } } pub fn estimate_token_length(text: &str) -> usize { let words: Vec<&str> = text.unicode_words().collect(); let mut output: f32 = 0.0; for word in words { if word.is_ascii() { output += 1.3; } else { let count = word.chars().count(); if count == 1 { output += 1.0 } else { output += (count as f32) * 0.5; } } } output.ceil() as usize } pub fn strip_think_tag(text: &str) -> Cow<'_, str> { THINK_TAG_RE.replace_all(text, "") } pub fn extract_code_block(text: &str) -> &str { CODE_BLOCK_RE .captures(text) .ok() .and_then(|v| v?.get(1).map(|v| v.as_str().trim())) .unwrap_or(text) } pub fn convert_option_string(value: &str) -> Option { if value.is_empty() { None } else { Some(value.to_string()) } } pub fn fuzzy_filter(values: Vec, get: F, pattern: &str) -> Vec where F: Fn(&T) -> &str, { let matcher = SkimMatcherV2::default(); let mut list: Vec<(T, i64)> = values .into_iter() .filter_map(|v| { let score = matcher.fuzzy_match(get(&v), pattern)?; Some((v, score)) }) .collect(); list.sort_unstable_by(|a, b| b.1.cmp(&a.1)); list.into_iter().map(|(v, _)| v).collect() } pub fn pretty_error(err: &anyhow::Error) -> String { let mut output = vec![]; output.push(format!("Error: {err}")); let causes: Vec<_> = err.chain().skip(1).collect(); let causes_len = causes.len(); if causes_len > 0 { output.push("\nCaused by:".to_string()); if causes_len == 1 { output.push(format!(" {}", indent_text(causes[0], 4).trim())); } else { for (i, cause) in causes.into_iter().enumerate() { output.push(format!("{i:5}: {}", indent_text(cause, 7).trim())); } } } output.join("\n") } pub fn indent_text(s: T, size: usize) -> String { let indent_str = " ".repeat(size); s.to_string() .split('\n') .map(|line| format!("{indent_str}{line}")) .collect::>() .join("\n") } pub fn error_text(input: &str) -> String { color_text(input, nu_ansi_term::Color::Red) } pub fn warning_text(input: &str) -> String { color_text(input, nu_ansi_term::Color::Yellow) } pub fn color_text(input: &str, color: nu_ansi_term::Color) -> String { if *NO_COLOR { return input.to_string(); } nu_ansi_term::Style::new() .fg(color) .paint(input) .to_string() } pub fn dimmed_text(input: &str) -> String { if *NO_COLOR { return input.to_string(); } nu_ansi_term::Style::new().dimmed().paint(input).to_string() } pub fn multiline_text(input: &str) -> String { input .split('\n') .enumerate() .map(|(i, v)| { if i == 0 { v.to_string() } else { format!(".. {v}") } }) .collect::>() .join("\n") } pub fn temp_file(prefix: &str, suffix: &str) -> PathBuf { env::temp_dir().join(format!( "{}-{}{prefix}{}{suffix}", env!("CARGO_CRATE_NAME").to_lowercase(), process::id(), uuid::Uuid::new_v4() )) } pub fn is_url(path: &str) -> bool { path.starts_with("http://") || path.starts_with("https://") } pub fn set_proxy( mut builder: reqwest::ClientBuilder, proxy: &str, ) -> Result { builder = builder.no_proxy(); if !proxy.is_empty() && proxy != "-" { builder = builder .proxy(reqwest::Proxy::all(proxy).with_context(|| format!("Invalid proxy `{proxy}`"))?); }; Ok(builder) } pub fn decode_bin(data: &[u8]) -> Result { let (v, _) = bincode::serde::decode_from_slice(data, bincode::config::legacy())?; Ok(v) } #[cfg(test)] mod tests { use super::*; #[test] #[cfg(not(target_os = "windows"))] fn test_safe_join_path() { assert_eq!( safe_join_path("/home/user/dir1", "files/file1"), Some(PathBuf::from("/home/user/dir1/files/file1")) ); assert!(safe_join_path("/home/user/dir1", "/files/file1").is_none()); assert!(safe_join_path("/home/user/dir1", "../file1").is_none()); } #[test] #[cfg(target_os = "windows")] fn test_safe_join_path() { assert_eq!( safe_join_path("C:\\Users\\user\\dir1", "files/file1"), Some(PathBuf::from("C:\\Users\\user\\dir1\\files\\file1")) ); assert!(safe_join_path("C:\\Users\\user\\dir1", "/files/file1").is_none()); assert!(safe_join_path("C:\\Users\\user\\dir1", "../file1").is_none()); } }