mirror of
https://github.com/hykilpikonna/hyfetch.git
synced 2026-08-08 03:34:54 -05:00
Merge a7b1eb6b83 into 202af90451
This commit is contained in:
commit
f4f7f1a5ad
|
|
@ -231,47 +231,51 @@ BACKEND={{{backends}}}",
|
|||
.version(env!("CARGO_PKG_VERSION"));
|
||||
}
|
||||
|
||||
/// Build a sorted completion list that ranks prefix matches before other substring matches.
|
||||
///
|
||||
/// Each entry is `(name, description)` where description is `Some(hint)` when provided.
|
||||
/// Prefix matches sort before other substring matches; within each group results are sorted
|
||||
/// by the position of the match and then alphabetically.
|
||||
#[cfg(feature = "autocomplete")]
|
||||
fn ranked_completions<'a>(
|
||||
candidates: impl Iterator<Item = &'a &'a str>,
|
||||
input: &str,
|
||||
description: Option<&str>,
|
||||
) -> Vec<(String, Option<String>)> {
|
||||
let desc = description.map(str::to_owned);
|
||||
let mut matched: Vec<(bool, usize, &str)> = candidates
|
||||
.filter_map(|&name| {
|
||||
name.find(input).map(|pos| (name.starts_with(input), pos, name))
|
||||
})
|
||||
.collect();
|
||||
// Prefix matches first (true sorts after false, so negate), then earliest position, then name.
|
||||
matched.sort_by_key(|&(is_prefix, pos, name)| (!is_prefix, pos, name));
|
||||
matched
|
||||
.into_iter()
|
||||
.map(|(_, _, name)| (name.to_owned(), desc.clone()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(feature = "autocomplete")]
|
||||
fn complete_preset(input: &String) -> Vec<(String, Option<String>)> {
|
||||
<Preset as VariantNames>::VARIANTS
|
||||
let all_variants: Vec<&str> = <Preset as VariantNames>::VARIANTS
|
||||
.iter()
|
||||
.chain(iter::once(&"random"))
|
||||
.filter_map(|&name| {
|
||||
if name.starts_with(input) {
|
||||
Some((name.to_owned(), None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.copied()
|
||||
.chain(iter::once("random"))
|
||||
.collect();
|
||||
ranked_completions(all_variants.iter(), input.as_str(), Some("pride flag preset"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "autocomplete")]
|
||||
fn complete_mode(input: &String) -> Vec<(String, Option<String>)> {
|
||||
AnsiMode::VARIANTS
|
||||
.iter()
|
||||
.filter_map(|&name| {
|
||||
if name.starts_with(input) {
|
||||
Some((name.to_owned(), None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
let variants: Vec<&str> = AnsiMode::VARIANTS.to_vec();
|
||||
ranked_completions(variants.iter(), input.as_str(), Some("color mode"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "autocomplete")]
|
||||
fn complete_backend(input: &String) -> Vec<(String, Option<String>)> {
|
||||
Backend::VARIANTS
|
||||
.iter()
|
||||
.filter_map(|&name| {
|
||||
if name.starts_with(input) {
|
||||
Some((name.to_owned(), None))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
let variants: Vec<&str> = Backend::VARIANTS.to_vec();
|
||||
ranked_completions(variants.iter(), input.as_str(), Some("fetch backend"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -282,4 +286,43 @@ mod tests {
|
|||
fn check_options() {
|
||||
options().check_invariants(false)
|
||||
}
|
||||
|
||||
#[cfg(feature = "autocomplete")]
|
||||
#[test]
|
||||
fn complete_preset_substring() {
|
||||
// "gender" is a substring of "transgender" but not a prefix → must still match
|
||||
let results = complete_preset(&"gender".to_owned());
|
||||
let names: Vec<&str> = results.iter().map(|(n, _)| n.as_str()).collect();
|
||||
assert!(
|
||||
names.contains(&"transgender"),
|
||||
"substring 'gender' should match 'transgender', got: {names:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "autocomplete")]
|
||||
#[test]
|
||||
fn complete_preset_prefix_ranked_first() {
|
||||
// "trans" is a prefix of "transgender" → it should appear before any non-prefix matches
|
||||
let results = complete_preset(&"trans".to_owned());
|
||||
assert!(!results.is_empty(), "expected at least one result for 'trans'");
|
||||
let first = results[0].0.as_str();
|
||||
assert!(
|
||||
first.starts_with("trans"),
|
||||
"first result should be a prefix match, got: {first}"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "autocomplete")]
|
||||
#[test]
|
||||
fn complete_preset_descriptions() {
|
||||
// Every completion result should carry a non-empty description
|
||||
let results = complete_preset(&"rain".to_owned());
|
||||
assert!(!results.is_empty(), "expected at least one result for 'rain'");
|
||||
for (name, desc) in &results {
|
||||
assert!(
|
||||
desc.as_deref().is_some_and(|d| !d.is_empty()),
|
||||
"completion for '{name}' should have a description"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -198,15 +198,27 @@ class RGB:
|
|||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def to_ansi(self, mode: AnsiMode | None = None, foreground: bool = True):
|
||||
def to_ansi(self, mode: AnsiMode | None = None, foreground: bool = True) -> str:
|
||||
if not mode:
|
||||
mode = GLOBAL_CFG.color_mode
|
||||
|
||||
# If the mode is none, raise an error
|
||||
if mode is None:
|
||||
raise ValueError("GLOBAL_CFG.color_mode is not set")
|
||||
|
||||
if mode == 'rgb':
|
||||
return self.to_ansi_rgb(foreground)
|
||||
if mode == '8bit':
|
||||
return self.to_ansi_8bit(foreground)
|
||||
if mode == 'default':
|
||||
# treat 'default' as 8bit (256 colors)
|
||||
return self.to_ansi_8bit(foreground)
|
||||
if mode == 'ansi':
|
||||
return self.to_ansi_16(foreground)
|
||||
# 16-color 'ansi' mode is not implemented yet.
|
||||
raise NotImplementedError("'ansi' (16-color) mode is not implemented")
|
||||
|
||||
# Unknown / misspelled mode in config
|
||||
raise ValueError(f"Unknown color mode: {mode!r}")
|
||||
|
||||
def lighten(self, multiplier: float) -> 'RGB':
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -289,10 +289,12 @@ def create_config() -> Config:
|
|||
return def_lightness
|
||||
|
||||
try:
|
||||
if lightness.endswith('%') or int(lightness) > 1:
|
||||
lightness = int(lightness[:-1]) / 100 if lightness.endswith('%') else int(lightness) / 100
|
||||
if lightness.endswith('%'):
|
||||
lightness = int(lightness[:-1]) / 100
|
||||
else:
|
||||
lightness = float(lightness)
|
||||
# Accept plain floats (.45, 0.45) and integers treated as percentages (45 → 0.45)
|
||||
value = float(lightness)
|
||||
lightness = value / 100 if value > 1 else value
|
||||
assert 0 <= lightness <= 1
|
||||
return lightness
|
||||
|
||||
|
|
@ -533,7 +535,8 @@ def run():
|
|||
now = datetime.datetime.now()
|
||||
june_path = CACHE_PATH / f'animation-displayed-{now.year}'
|
||||
show_for_june = False
|
||||
if now.month == 6 and now.year not in config.pride_month_shown and not june_path.is_file() and os.isatty(sys.stdout.fileno()):
|
||||
stdout_is_tty = hasattr(sys.stdout, 'isatty') and sys.stdout.isatty()
|
||||
if now.month == 6 and now.year not in config.pride_month_shown and not june_path.is_file() and stdout_is_tty:
|
||||
show_for_june = True
|
||||
|
||||
if (args.june or show_for_june) and not config.pride_month_disable:
|
||||
|
|
|
|||
|
|
@ -226,7 +226,7 @@ def ensure_git_bash() -> Path:
|
|||
or if_file("C:/Program Files/Git/bin/bash.exe")
|
||||
or if_file("C:/Program Files (x86)/Git/bin/bash.exe"))
|
||||
|
||||
if not git_path.is_file():
|
||||
if not git_path or not git_path.is_file():
|
||||
printc("&cError: Git Bash installation not found")
|
||||
sys.exit(127)
|
||||
|
||||
|
|
@ -299,6 +299,9 @@ def get_distro_ascii(distro: str | None = None) -> str:
|
|||
cmd += f' --ascii_distro {distro}'
|
||||
|
||||
asc = run_neofetch_cmd(cmd, True)
|
||||
if not asc:
|
||||
printc("&cError: Failed to get ASCII art from neofetch")
|
||||
sys.exit(1)
|
||||
|
||||
# Unescape backslashes here because backslashes are escaped in neofetch for printf
|
||||
asc = asc.replace('\\\\', '\\')
|
||||
|
|
@ -319,6 +322,7 @@ def run(asc: str, backend: BackendLiteral, args: str = ''):
|
|||
return run_fastfetch(asc, args, legacy=True)
|
||||
if backend == "qwqfetch":
|
||||
return run_qwqfetch(asc, args)
|
||||
raise ValueError(f"Unknown backend: {backend!r}")
|
||||
|
||||
|
||||
def run_qwqfetch(asc: str, args: str = ''):
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ def unix_detect_ansi_mode() -> AnsiMode | None:
|
|||
if hasattr(sys.stdout, 'isatty') and not sys.stdout.isatty():
|
||||
return 'ansi'
|
||||
|
||||
term = os.environ.get('TERM')
|
||||
color_term = os.environ.get('COLORTERM')
|
||||
term = os.environ.get('TERM') or ''
|
||||
color_term = os.environ.get('COLORTERM') or ''
|
||||
|
||||
if color_term == 'truecolor' or color_term == '24bit':
|
||||
if term.startswith('screen') and os.environ.get('TERM_PROGRAM') != 'tmux':
|
||||
|
|
@ -61,12 +61,16 @@ def windows_detect_ansi_mode() -> AnsiMode | None:
|
|||
if os.environ.get("ConEmuANSI") == "ON":
|
||||
return 'rgb'
|
||||
|
||||
release, _, build = map(int, platform.version().split('.'))
|
||||
try:
|
||||
release, _, build = map(int, platform.version().split('.'))
|
||||
except (ValueError, TypeError):
|
||||
# If the version string is unparseable, assume a modern Windows with full color support.
|
||||
return 'rgb'
|
||||
if build < 10586 or release < 10:
|
||||
# No ANSI support before Windows 10 build 10586.
|
||||
if os.environ.get('ANSICON'):
|
||||
conv = os.environ.get('ANSICON_VER')
|
||||
if int(conv) < 181:
|
||||
conv = os.environ.get('ANSICON_VER') or ''
|
||||
if conv.isdigit() and int(conv) < 181:
|
||||
return 'ansi'
|
||||
return '8bit'
|
||||
return 'ansi'
|
||||
|
|
@ -95,7 +99,7 @@ def unix_read_osc(seq: int) -> str:
|
|||
|
||||
# screen/tmux can't support OSC, because they can be connected to multiple
|
||||
# terminals concurrently.
|
||||
term = os.environ.get('TERM')
|
||||
term = os.environ.get('TERM') or ''
|
||||
if term.startswith("screen") or term.startswith("tmux"):
|
||||
raise OSCException("Screen/tmux not supported")
|
||||
|
||||
|
|
@ -152,8 +156,12 @@ def unix_read_osc(seq: int) -> str:
|
|||
if not code.startswith(start):
|
||||
raise OSCException("Received response is not an OSC response")
|
||||
|
||||
# Strip starting code and termination code
|
||||
code = code.lstrip(start).rstrip("\x1b\\").rstrip('\a')
|
||||
# Strip starting prefix and trailing termination sequence
|
||||
code = code[len(start):]
|
||||
if code.endswith("\x1b\\"):
|
||||
code = code[:-2]
|
||||
elif code.endswith('\a'):
|
||||
code = code[:-1]
|
||||
|
||||
return code
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ except ImportError:
|
|||
|
||||
AnsiMode = Literal['default', 'ansi', '8bit', 'rgb']
|
||||
LightDark = Literal['light', 'dark']
|
||||
BackendLiteral = Literal["neofetch", "fastfetch"]
|
||||
BackendLiteral = Literal["qwqfetch", "neofetch", "fastfetch", "fastfetch-old"]
|
||||
ColorAlignMode = Literal['horizontal', 'vertical', 'custom']
|
||||
ColorSpacing = Literal['equal', 'weighted']
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user