mirror of
https://github.com/hykilpikonna/hyfetch.git
synced 2026-08-22 08:45:38 -05:00
[F] Fix multiple crash bugs in the Python fallback
termenv.py:
- unix_detect_ansi_mode: `TERM`/`COLORTERM` can be absent from the
environment (CI, cron, some IDEs). `os.environ.get('TERM')` returns
None in that case, so subsequent `term.startswith(...)` and
`'256color' in term` calls raised AttributeError. Changed to
`os.environ.get('TERM') or ''` for both variables.
- unix_read_osc: same None dereference on line 99 (`term.startswith`).
Applied the same `or ''` guard.
- unix_read_osc: `code.lstrip(start)` was used to strip the OSC prefix,
but `str.lstrip` strips individual *characters*, not a prefix string,
so it could silently over-consume leading bytes of the actual payload.
Replaced with an explicit `code[len(start):]` slice after a
`startswith` check.
- windows_detect_ansi_mode: `map(int, platform.version().split('.'))`
crashes with ValueError/TypeError on non-standard version strings.
Wrapped in try/except with a safe fallback of `'rgb'`.
`int(os.environ.get('ANSICON_VER'))` raised when the var was unset or
non-numeric; guarded with `.isdigit()` before converting.
color_util.py:
- RGB.to_ansi: for `mode == 'ansi'` it forwarded to `to_ansi_16` which
is an unimplemented stub (`raise NotImplementedError`). For
`mode == 'default'` and any other unknown mode the function fell
through and returned None, causing TypeError when callers concatenated
the result into strings (e.g. in presets.py). Both cases now fall back
to `to_ansi_8bit`, which is a correct and safe 256-color degradation.
Return type annotation updated to `str`.
neofetch_util.py:
- ensure_git_bash: `git_path` returned by the `if_file(...)` chain can
be None when no Git Bash installation is found on Windows. The
subsequent `git_path.is_file()` then raised AttributeError instead of
printing the friendly error message. Fixed to `if not git_path or not
git_path.is_file():`.
- get_distro_ascii: `run_neofetch_cmd` can return None when neofetch is
missing or exits non-zero without raising. The following `.replace()`
call would then crash with AttributeError. Added an explicit None
guard that prints an error and exits cleanly.
- run(): the backend dispatcher had no else/fallback branch, so an
unknown or mistyped backend silently returned None and produced no
output. Added `raise ValueError(f"Unknown backend: {backend!r}")`.
types.py:
- BackendLiteral was `Literal["neofetch", "fastfetch"]`, missing
`"qwqfetch"` and `"fastfetch-old"` which are both accepted by the
argparse parser and the run() dispatcher. Stale types caused false
type-checker warnings for valid inputs. Updated to include all four
supported backends.
main.py:
- select_lightness: the prompt advertises `.45` and `0.45` as valid
decimal inputs, but the parser called `int(lightness)` first, which
raises ValueError for any non-integer float, landing in the error
path before the `float()` branch was ever reached. Reordered to:
handle `%` suffix first, then `float()` for everything else, dividing
by 100 only when the value exceeds 1.
- June/pride-month check: `os.isatty(sys.stdout.fileno())` raises
io.UnsupportedOperation when stdout is piped or replaced (e.g. during
testing or when output is redirected). Replaced with the pattern
already used in termenv.py: `hasattr(sys.stdout, 'isatty') and
sys.stdout.isatty()`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -198,15 +198,16 @@ 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 mode == 'rgb':
|
||||
return self.to_ansi_rgb(foreground)
|
||||
if mode == '8bit':
|
||||
return self.to_ansi_8bit(foreground)
|
||||
if mode == 'ansi':
|
||||
return self.to_ansi_16(foreground)
|
||||
# 'ansi' (16-color) is not yet implemented; fall back to 8bit which always works.
|
||||
# 'default' and any unknown mode also fall through here as a safe degradation.
|
||||
return self.to_ansi_8bit(foreground)
|
||||
|
||||
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']
|
||||
|
||||
Reference in New Issue
Block a user