This commit is contained in:
flaykDev 2026-07-12 04:41:58 +00:00 committed by GitHub
commit f4f7f1a5ad
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 116 additions and 46 deletions

View File

@ -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"
);
}
}
}

View File

@ -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':
"""

View File

@ -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:

View File

@ -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 = ''):

View File

@ -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

View File

@ -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']