Baseline project
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
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::{skim::SkimMatcherV2, FuzzyMatcher};
|
||||
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<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?ms)```\w*(.*)```").unwrap());
|
||||
pub static THINK_TAG_RE: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(?s)^\s*<think>.*?</think>(\s*|$)").unwrap());
|
||||
pub static IS_STDOUT_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stdout().is_terminal());
|
||||
pub static NO_COLOR: LazyLock<bool> = 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<bool> {
|
||||
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<String> {
|
||||
if value.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(value.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fuzzy_filter<T, F>(values: Vec<T>, get: F, pattern: &str) -> Vec<T>
|
||||
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<T: ToString>(s: T, size: usize) -> String {
|
||||
let indent_str = " ".repeat(size);
|
||||
s.to_string()
|
||||
.split('\n')
|
||||
.map(|line| format!("{indent_str}{line}"))
|
||||
.collect::<Vec<String>>()
|
||||
.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::<Vec<String>>()
|
||||
.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<reqwest::ClientBuilder> {
|
||||
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<T: serde::de::DeserializeOwned>(data: &[u8]) -> Result<T> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user