From 49eaebcde012e5955446739f723a10299dcc7936 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Mon, 8 Jun 2026 21:26:48 +0300 Subject: [PATCH 01/25] started work on tokenizer + ansi processor I still am not sure whether this will actually fix it but oh well. --- .gitignore | 2 +- pyproject.toml | 2 +- src/anifetch/ansi_process.py | 104 +++++++++++++++++++++++++++++++++++ src/anifetch/core.py | 3 + src/anifetch/utils.py | 10 ++++ 5 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/anifetch/ansi_process.py diff --git a/.gitignore b/.gitignore index b609f70..c61a783 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ .wakatime-project .ruff_cache/ .flatpak-builder/ -.ignore/ +*.ignore* .direnv/ # Byte-compiled / optimized / DLL files diff --git a/pyproject.toml b/pyproject.toml index 12ebdb0..62af80b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ keywords = [ "fastfetch", "anifetch", ] -version = "1.0.4" +version = "1.0.5" description = "Animated terminal fetch with video and audio support." # If I put everyone here it messes with the "author" meta section. authors = [{ name = "Notenlish", email = "notenlish@gmail.com" }] diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py new file mode 100644 index 0000000..f453b5e --- /dev/null +++ b/src/anifetch/ansi_process.py @@ -0,0 +1,104 @@ +from typing import Literal +import re +from .utils import debug_write_str, overwrite_string + + +class Token: + def __init__(self, type_:Literal["text", "go_left", "go_right"], value) -> None: + self.type = type_ + self.value: str|int = value + + def __repr__(self) -> str: + if self.type == "text": + # pyrefly: ignore [bad-return] + return self.value + else: + code = "C" if self.type == "go_left" else "D" + return f"<{self.type} {self.value}>" + # return f"\x1b[{self.value}{code}" + +def tokenize_lines(lines:list[str]): + lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] + ## attempt to tokenize every line. + pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" + + + line_tokens_all: list[list[Token]] = [] + + for line in lines: + line_tokens: list[Token] = [] + text_index = 0 + + for match in re.finditer(pattern, line): + start, end = match.span() + + # if there is text before the escape sequence + if start > text_index: + line_tokens.append(Token("text", line[text_index:start])) + + match_text = line[start:end] + + debug_write_str(f"MATCH.STRING\n\n {match_text}") + # example: \x1b[100D + _left = match_text.find("[") + 1 + _right = len(match_text) -1 + # print(f"lets see what this is?? \n\n {match_text[_left: _right]}") + amount = int(match_text[_left: _right]) + + code = match_text[-1] + + if code == "C": + line_tokens.append(Token("go_right", amount)) + elif code == "D": + line_tokens.append(Token("go_left", amount)) + #else: + # continue + + text_index = end + + # check if there is remaining text after the last escape sequence match. + if text_index < len(line): + line_tokens.append(Token("text", line[text_index:])) + + line_tokens_all.append(line_tokens) + return line_tokens_all + +def expand_ansi_movement_seq(lines:list[str]): + line_tokens_all = tokenize_lines(lines) + # print(line_tokens_all) + + lines = [] + + for line_tokens in line_tokens_all: + line = "" + cur_i = 0 # cursor pos(x) + for token in line_tokens: + if token.type == "text": + # this doesn't overwrite text + # line = line[:cur_i] + token.value + line[cur_i:] + line = overwrite_string(line, cur_i, token.value) + cur_i += len(token.value) + if token.type == "go_right": + # pyrefly: ignore [unsupported-operation] + wanted_i = cur_i + token.value + max_i = len(line) + needed_space = max(wanted_i - max_i, 0) + line += " " * needed_space + cur_i = wanted_i + if token.type == "go_left": + # cur_i = 10 + # token.value = 20(wants to go 20 left) + # max_i = 20 + # min_i = 0 + # wanted_i = -10 + + # pyrefly: ignore [unsupported-operation] + wanted_i = cur_i - token.value + min_i = 0 + needed_space = max(wanted_i * -1, 0) # 10 + line = " " * needed_space + line + cur_i = wanted_i + needed_space # 0 + lines.append(line) + + print(lines) + return lines diff --git a/src/anifetch/core.py b/src/anifetch/core.py index 2594cca..ba78c68 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -2,6 +2,7 @@ Anifetch core module for running the animation. """ +from anifetch.ansi_process import expand_ansi_movement_seq import json import os import pathlib @@ -261,6 +262,8 @@ def run_anifetch(args): fetch_output: list[str] = get_fetch_output( not args.neofetch, neofetch_status, args.force, args.config ) + expand_ansi_movement_seq(fetch_output) + raise SystemExit # copy fetch_output to fetch_lines fetch_lines: list[str] = fetch_output[:] diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index e6d0958..c467d20 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -711,3 +711,13 @@ def split_to_frames(args, CACHE_PATH, IS_TRANSPARENT, stdout, stderr): stderr=stderr, text=True, ) + + +def debug_write_str(t:str): + with open("debug.ignore", "w") as f: + f.write(t) + +def overwrite_string(og_text:str, index:int, text_to_overwrite:str): + out = og_text[:index] + text_to_overwrite + og_text[index + len(text_to_overwrite):] + + return out From 89161c0879183c8e638ef760f4fbb8766d9d2f53 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:52:35 +0300 Subject: [PATCH 02/25] Update pyproject.toml --- pyproject.toml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 62af80b..f878bbf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,3 @@ -# TODO: publish on pypi - [build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" @@ -38,7 +36,7 @@ maintainers = [ readme = "README.md" license = { text = "MIT" } requires-python = ">=3.11" -dependencies = ["platformdirs", "wcwidth", "rich", "pynput"] +dependencies = ["platformdirs==4.5.1", "wcwidth==0.2.14", "rich==14.3.1", "pynput==1.8.1","ansi2txt==0.2.0"] [project.urls] Homepage = "https://github.com/Notenlish/anifetch" From 54ba4fec139f06f7f30570183e7994ddfde9e854 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:52:43 +0300 Subject: [PATCH 03/25] Update ansi_process.py --- src/anifetch/ansi_process.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index f453b5e..49d3767 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -18,7 +18,7 @@ def __repr__(self) -> str: # return f"\x1b[{self.value}{code}" def tokenize_lines(lines:list[str]): - lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] + # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] ## attempt to tokenize every line. pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" @@ -64,8 +64,8 @@ def tokenize_lines(lines:list[str]): return line_tokens_all def expand_ansi_movement_seq(lines:list[str]): + # debug_write_str("\n\n\n".join(lines)) line_tokens_all = tokenize_lines(lines) - # print(line_tokens_all) lines = [] @@ -100,5 +100,6 @@ def expand_ansi_movement_seq(lines:list[str]): cur_i = wanted_i + needed_space # 0 lines.append(line) - print(lines) + debug_write_str("\n".join(lines)) + print("\n".join(lines)) return lines From d15b3b0730b4b77bb48963577c984e5963b0cd1e Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:52:48 +0300 Subject: [PATCH 04/25] Update core.py --- src/anifetch/core.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/anifetch/core.py b/src/anifetch/core.py index ba78c68..07b6e2e 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -262,7 +262,8 @@ def run_anifetch(args): fetch_output: list[str] = get_fetch_output( not args.neofetch, neofetch_status, args.force, args.config ) - expand_ansi_movement_seq(fetch_output) + + # expand_ansi_movement_seq(fetch_output) raise SystemExit # copy fetch_output to fetch_lines From d9d7903ec7ea0c2c3b4717fcbeed088ebf92e0c1 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:52:51 +0300 Subject: [PATCH 05/25] Update utils.py --- src/anifetch/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index c467d20..f2fd017 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -714,7 +714,7 @@ def split_to_frames(args, CACHE_PATH, IS_TRANSPARENT, stdout, stderr): def debug_write_str(t:str): - with open("debug.ignore", "w") as f: + with open("debug.ignore", "w", encoding="utf-8") as f: f.write(t) def overwrite_string(og_text:str, index:int, text_to_overwrite:str): From 5b58751e814e313b19aa13894acd5a730418fc3a Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:54:20 +0300 Subject: [PATCH 06/25] Update pyproject.toml --- pyproject.toml | 89 ++++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 42 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f878bbf..01410e1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,54 +1,59 @@ [build-system] -requires = ["setuptools>=61.0"] -build-backend = "setuptools.build_meta" + requires = ["setuptools>=61.0"] + build-backend = "setuptools.build_meta" [project] -name = "anifetch-cli" -classifiers = [ - "Development Status :: 5 - Production/Stable", - "Environment :: Console", - "Topic :: Terminals", - "Operating System :: Microsoft :: Windows", - "Operating System :: POSIX", - "Operating System :: Unix", - "Operating System :: MacOS", -] -keywords = [ - "fetch", - "terminal", - "cli", - "animated", - "neofetch", - "fastfetch", - "anifetch", -] -version = "1.0.5" -description = "Animated terminal fetch with video and audio support." -# If I put everyone here it messes with the "author" meta section. -authors = [{ name = "Notenlish", email = "notenlish@gmail.com" }] -maintainers = [ - # If I make it start with immelancholy for some reason it uses gallophostix's gmail for immelancholy. - { name = "Gallophostrix", email = "gallophostrix@gmail.com" }, - { name = "Immelancholy" }, - { name = "Notenlish", email = "notenlish@gmail.com" }, -] + name = "anifetch-cli" + classifiers = [ + "Development Status :: 5 - Production/Stable", + "Environment :: Console", + "Topic :: Terminals", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", + ] + keywords = [ + "fetch", + "terminal", + "cli", + "animated", + "neofetch", + "fastfetch", + "anifetch", + ] + version = "1.0.5" + description = "Animated terminal fetch with video and audio support." + # If I put everyone here it messes with the "author" meta section. + authors = [{ name = "Notenlish", email = "notenlish@gmail.com" }] + maintainers = [ + # If I make it start with immelancholy for some reason it uses gallophostix's gmail for immelancholy. + { name = "Gallophostrix", email = "gallophostrix@gmail.com" }, + { name = "Immelancholy" }, + { name = "Notenlish", email = "notenlish@gmail.com" }, + ] -readme = "README.md" -license = { text = "MIT" } -requires-python = ">=3.11" -dependencies = ["platformdirs==4.5.1", "wcwidth==0.2.14", "rich==14.3.1", "pynput==1.8.1","ansi2txt==0.2.0"] + readme = "README.md" + license = { text = "MIT" } + requires-python = ">=3.11" + dependencies = [ + "platformdirs==4.5.1", + "wcwidth==0.2.14", + "rich==14.3.1", + "pynput==1.8.1", + ] [project.urls] -Homepage = "https://github.com/Notenlish/anifetch" -Issues = "https://github.com/Notenlish/anifetch/issues" + Homepage = "https://github.com/Notenlish/anifetch" + Issues = "https://github.com/Notenlish/anifetch/issues" [project.scripts] -anifetch = "anifetch.__init__:main" -anifetch-cli = "anifetch.__init__:main" + anifetch = "anifetch.__init__:main" + anifetch-cli = "anifetch.__init__:main" [tool.setuptools] -packages = ["anifetch"] -package-dir = { "" = "src" } + packages = ["anifetch"] + package-dir = { "" = "src" } [tool.setuptools.package-data] -anifetch = ["assets/**/*"] + anifetch = ["assets/**/*"] From cf7e3bb5d53403dcc119a6e940e0b04acd03b4f1 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 12:54:24 +0300 Subject: [PATCH 07/25] Create ansi2txt.py --- src/anifetch/ansi2txt.py | 55 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 src/anifetch/ansi2txt.py diff --git a/src/anifetch/ansi2txt.py b/src/anifetch/ansi2txt.py new file mode 100644 index 0000000..619916b --- /dev/null +++ b/src/anifetch/ansi2txt.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python + +# SPDX-License-Identifier: MIT AND AGPL-3.0-only + +# Original code from https://github.com/mmlb/ansi2txt + +def ansi2txt(text: str) -> str: + EOF = "" + pos = 0 + output = [] + + def getchar(): + nonlocal pos + if pos >= len(text): + return EOF + ch = text[pos] + pos += 1 + return ch + + ch = None + + while ch != EOF: + ch = getchar() + + while ch == "\r": + ch = getchar() + if ch != "\n": + output.append("\r") + + if ch == "\x1b": + ch = getchar() + + if ch == "[": + ch = getchar() + while ch == ";" or ("0" <= ch <= "9") or ch == "?": + ch = getchar() + + elif ch == "]": + ch = getchar() + if ch != EOF and "0" <= ch <= "9": + while True: + ch = getchar() + if ch == EOF or ord(ch) == 7: + break + elif ch == "\x1b": + ch = getchar() + break + + elif ch == "%": + ch = getchar() + + elif ch != EOF: + output.append(ch) + + return "".join(output) \ No newline at end of file From e619b927d54ecba8eb454170161606780a7f4afd Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:27:58 +0300 Subject: [PATCH 08/25] im getting trolled by ansi escape sequences --- src/anifetch/ansi_process.py | 3 +++ src/anifetch/core.py | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index 49d3767..42798db 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -1,6 +1,8 @@ from typing import Literal import re from .utils import debug_write_str, overwrite_string +from .ansi2txt import ansi2txt + class Token: @@ -77,6 +79,7 @@ def expand_ansi_movement_seq(lines:list[str]): # this doesn't overwrite text # line = line[:cur_i] + token.value + line[cur_i:] line = overwrite_string(line, cur_i, token.value) + cur_i += len(token.value) if token.type == "go_right": # pyrefly: ignore [unsupported-operation] diff --git a/src/anifetch/core.py b/src/anifetch/core.py index 07b6e2e..96ad27c 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -262,8 +262,8 @@ def run_anifetch(args): fetch_output: list[str] = get_fetch_output( not args.neofetch, neofetch_status, args.force, args.config ) - - # expand_ansi_movement_seq(fetch_output) + + expand_ansi_movement_seq(fetch_output) raise SystemExit # copy fetch_output to fetch_lines From 1583d6c2ced0c22d53620dcac158a68bbc333a58 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Tue, 9 Jun 2026 22:27:23 +0300 Subject: [PATCH 09/25] 1 step closer to a fix --- pyproject.toml | 1 + src/anifetch/ansi_process.py | 31 ++++++++++++++++++++++++++++--- src/anifetch/core.py | 4 ++-- src/anifetch/utils.py | 4 ++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01410e1..5b69d0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,6 +41,7 @@ "wcwidth==0.2.14", "rich==14.3.1", "pynput==1.8.1", + "pyte==0.8.2" ] [project.urls] diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index 42798db..c63f83a 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -6,7 +6,7 @@ class Token: - def __init__(self, type_:Literal["text", "go_left", "go_right"], value) -> None: + def __init__(self, type_:Literal["text", "go_left", "go_right", "move_to_column"], value) -> None: self.type = type_ self.value: str|int = value @@ -19,10 +19,20 @@ def __repr__(self) -> str: return f"<{self.type} {self.value}>" # return f"\x1b[{self.value}{code}" +# How to fix this +# 1 Proper handling of text length via wcwidth(color escape sequences mess with the strings) +# 2 proper handling of emojis. When this 💻 emoji is present in the config, the code prints 1 more border than required + + +def strip_ansi_colors(lines: list[str]) -> list[str]: + ANSI_COLOR_RE = re.compile(r'\x1B\[[0-9;]*m') + return [ANSI_COLOR_RE.sub('', line) for line in lines] + def tokenize_lines(lines:list[str]): # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] ## attempt to tokenize every line. - pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" + # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" + pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsu]" line_tokens_all: list[list[Token]] = [] @@ -45,7 +55,10 @@ def tokenize_lines(lines:list[str]): _left = match_text.find("[") + 1 _right = len(match_text) -1 # print(f"lets see what this is?? \n\n {match_text[_left: _right]}") - amount = int(match_text[_left: _right]) + try: + amount = int(match_text[_left: _right]) + except ValueError: + amount = None code = match_text[-1] @@ -53,6 +66,9 @@ def tokenize_lines(lines:list[str]): line_tokens.append(Token("go_right", amount)) elif code == "D": line_tokens.append(Token("go_left", amount)) + elif code == "G": + if not amount: amount = 1 + line_tokens.append(Token("move_to_column", amount)) #else: # continue @@ -101,6 +117,15 @@ def expand_ansi_movement_seq(lines:list[str]): needed_space = max(wanted_i * -1, 0) # 10 line = " " * needed_space + line cur_i = wanted_i + needed_space # 0 + if token.type == "go_to_column": + # ANSI ESC[nG -> move to column n (1-based) + wanted_i = max(token.value, 1) - 1 # convert to 0-based + + # extend line if needed + if wanted_i > len(line): + line += " " * (wanted_i - len(line)) + + cur_i = wanted_i lines.append(line) debug_write_str("\n".join(lines)) diff --git a/src/anifetch/core.py b/src/anifetch/core.py index 96ad27c..ff321cd 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -2,7 +2,7 @@ Anifetch core module for running the animation. """ -from anifetch.ansi_process import expand_ansi_movement_seq +from .ansi_process import expand_ansi_movement_seq,strip_ansi_colors import json import os import pathlib @@ -262,7 +262,7 @@ def run_anifetch(args): fetch_output: list[str] = get_fetch_output( not args.neofetch, neofetch_status, args.force, args.config ) - + fetch_output = strip_ansi_colors(fetch_output) expand_ansi_movement_seq(fetch_output) raise SystemExit diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index f2fd017..09b1e3d 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -717,7 +717,7 @@ def debug_write_str(t:str): with open("debug.ignore", "w", encoding="utf-8") as f: f.write(t) -def overwrite_string(og_text:str, index:int, text_to_overwrite:str): - out = og_text[:index] + text_to_overwrite + og_text[index + len(text_to_overwrite):] +def overwrite_string(original_text:str, index:int, text_to_overwrite:str): + out = original_text[:index] + text_to_overwrite + original_text[index + len(text_to_overwrite):] return out From 04119d0dd76654821e6fa14aa7d2d9dfa1abd4ba Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Thu, 11 Jun 2026 00:21:04 +0300 Subject: [PATCH 10/25] so close --- src/anifetch/ansi_process.py | 17 ++++++++--------- src/anifetch/core.py | 2 +- src/anifetch/utils.py | 5 ++++- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index c63f83a..fc68de2 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -1,14 +1,13 @@ from typing import Literal import re -from .utils import debug_write_str, overwrite_string +from .utils import debug_write_str, overwrite_string, printable_len from .ansi2txt import ansi2txt - class Token: - def __init__(self, type_:Literal["text", "go_left", "go_right", "move_to_column"], value) -> None: + def __init__(self, type_:Literal["text", "go_left", "go_right", "go_to_column"], value) -> None: self.type = type_ - self.value: str|int = value + self.value: str|int = value def __repr__(self) -> str: if self.type == "text": @@ -68,7 +67,7 @@ def tokenize_lines(lines:list[str]): line_tokens.append(Token("go_left", amount)) elif code == "G": if not amount: amount = 1 - line_tokens.append(Token("move_to_column", amount)) + line_tokens.append(Token("go_to_column", amount)) #else: # continue @@ -96,11 +95,11 @@ def expand_ansi_movement_seq(lines:list[str]): # line = line[:cur_i] + token.value + line[cur_i:] line = overwrite_string(line, cur_i, token.value) - cur_i += len(token.value) + cur_i += printable_len(token.value) if token.type == "go_right": # pyrefly: ignore [unsupported-operation] wanted_i = cur_i + token.value - max_i = len(line) + max_i = printable_len(line) needed_space = max(wanted_i - max_i, 0) line += " " * needed_space cur_i = wanted_i @@ -122,8 +121,8 @@ def expand_ansi_movement_seq(lines:list[str]): wanted_i = max(token.value, 1) - 1 # convert to 0-based # extend line if needed - if wanted_i > len(line): - line += " " * (wanted_i - len(line)) + if wanted_i > printable_len(line): + line += " " * (wanted_i - printable_len(line)) cur_i = wanted_i lines.append(line) diff --git a/src/anifetch/core.py b/src/anifetch/core.py index ff321cd..72c5a6a 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -262,7 +262,7 @@ def run_anifetch(args): fetch_output: list[str] = get_fetch_output( not args.neofetch, neofetch_status, args.force, args.config ) - fetch_output = strip_ansi_colors(fetch_output) + # fetch_output = strip_ansi_colors(fetch_output) # if I strip ansi colors the output is nearly the same as fastfetch expand_ansi_movement_seq(fetch_output) raise SystemExit diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index 09b1e3d..fab2ace 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -712,12 +712,15 @@ def split_to_frames(args, CACHE_PATH, IS_TRANSPARENT, stdout, stderr): text=True, ) +def printable_len(raw:str): + """Returns printable length of the string.""" + return wcwidth.wcswidth(clean_ansi(raw)) def debug_write_str(t:str): with open("debug.ignore", "w", encoding="utf-8") as f: f.write(t) def overwrite_string(original_text:str, index:int, text_to_overwrite:str): - out = original_text[:index] + text_to_overwrite + original_text[index + len(text_to_overwrite):] + out = original_text[:index] + text_to_overwrite + original_text[index + printable_len(text_to_overwrite):] return out From 8df7e87436983ce3075fec1c0da59ffa3ed4979e Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Thu, 11 Jun 2026 10:25:55 +0300 Subject: [PATCH 11/25] Update utils.py --- src/anifetch/utils.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index fab2ace..8677b56 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -714,7 +714,11 @@ def split_to_frames(args, CACHE_PATH, IS_TRANSPARENT, stdout, stderr): def printable_len(raw:str): """Returns printable length of the string.""" - return wcwidth.wcswidth(clean_ansi(raw)) + cleaned = clean_ansi(raw) + w = wcwidth.wcswidth(cleaned) + + # it can return -1, so if it does return -1 do per character + return w if w >= 0 else sum(max(wcwidth.wcwidth(c), 0) for c in cleaned) def debug_write_str(t:str): with open("debug.ignore", "w", encoding="utf-8") as f: From 3c0de886c100eccf0d3219f19cbb36723bbba0ad Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 13:51:00 +0300 Subject: [PATCH 12/25] close to being done --- pyproject.toml | 2 +- src/anifetch/ansi_process.py | 143 +++++++++++++++++--------- src/anifetch/ansi_process2.py | 182 ++++++++++++++++++++++++++++++++++ src/anifetch/core.py | 5 +- src/anifetch/utils.py | 9 +- 5 files changed, 289 insertions(+), 52 deletions(-) create mode 100644 src/anifetch/ansi_process2.py diff --git a/pyproject.toml b/pyproject.toml index 5b69d0a..6310b52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ "wcwidth==0.2.14", "rich==14.3.1", "pynput==1.8.1", - "pyte==0.8.2" + "regex==2026.5.9" ] [project.urls] diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index fc68de2..c4adb41 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -2,10 +2,49 @@ import re from .utils import debug_write_str, overwrite_string, printable_len from .ansi2txt import ansi2txt +import wcwidth + + + + +_ANSI_RE = re.compile(r"(?:\x1B\[|\x9B)[\d;]*[A-Za-z]") + +def _active_ansi_state(line: str, up_to: int) -> str: + """ + Return all ANSI SGR (color/style) sequences active just before Python + position `up_to`. Prepending this to a tail restores the correct color + state even when the preceding text contains an ANSI reset. + """ + return "".join( + m.group() + for m in _ANSI_RE.finditer(line, 0, up_to) + if m.group()[-1] == "m" # only SGR codes affect color/style + ) + +def _col_to_char(line: str, col: int) -> int: + """ + Return the Python string index where visual column *col* begins in *line*. + ANSI sequences are zero-width; double-width characters (emoji, CJK) count as 2. + Returns len(line) if *col* exceeds the visual width of the string. + """ + i, c = 0, 0 + while i < len(line): + if c >= col: + break + m = _ANSI_RE.match(line, i) + if m: + i = m.end() # ANSI sequences are invisible aka: skip, don't count + else: + w = wcwidth.wcwidth(line[i]) + c += max(w, 0) # wcwidth returns -1 for non-printable; treat as 0-wide + i += 1 + return i + + class Token: - def __init__(self, type_:Literal["text", "go_left", "go_right", "go_to_column"], value) -> None: + def __init__(self, type_:Literal["text", "go_left", "go_right", "go_to_column", "sgr"], value) -> None: self.type = type_ self.value: str|int = value @@ -29,10 +68,9 @@ def strip_ansi_colors(lines: list[str]) -> list[str]: def tokenize_lines(lines:list[str]): # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] - ## attempt to tokenize every line. # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsu]" - + # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsum]" line_tokens_all: list[list[Token]] = [] @@ -49,15 +87,19 @@ def tokenize_lines(lines:list[str]): match_text = line[start:end] - debug_write_str(f"MATCH.STRING\n\n {match_text}") + # debug_write_str(f"MATCH.STRING\n\n {match_text}") + + bracket = match_text.find("[") # example: \x1b[100D - _left = match_text.find("[") + 1 + _left = (bracket + 1) if bracket != -1 else 1 _right = len(match_text) -1 - # print(f"lets see what this is?? \n\n {match_text[_left: _right]}") + + amount_str = match_text[_left:_right] + try: - amount = int(match_text[_left: _right]) + amount = int(amount_str) if amount_str else 1 except ValueError: - amount = None + amount = 1 code = match_text[-1] @@ -66,8 +108,8 @@ def tokenize_lines(lines:list[str]): elif code == "D": line_tokens.append(Token("go_left", amount)) elif code == "G": - if not amount: amount = 1 line_tokens.append(Token("go_to_column", amount)) + #else: # continue @@ -84,49 +126,60 @@ def expand_ansi_movement_seq(lines:list[str]): # debug_write_str("\n\n\n".join(lines)) line_tokens_all = tokenize_lines(lines) - lines = [] + result: list[str] = [] for line_tokens in line_tokens_all: line = "" - cur_i = 0 # cursor pos(x) + cur_col = 0 # visual terminal column + cur_char = 0 # string index(raw data) for token in line_tokens: if token.type == "text": - # this doesn't overwrite text - # line = line[:cur_i] + token.value + line[cur_i:] - line = overwrite_string(line, cur_i, token.value) - - cur_i += printable_len(token.value) + text: str = token.value # type: ignore[assignment] + vis_len = printable_len(text) + + if cur_char > len(line): + line += " " * (cur_char - len(line)) + + tail_char = _col_to_char(line, cur_col + vis_len) + + # Collect the SGR state active at tail_char so that border characters + # in the tail (spaces, ║) keep their original color even when the + # inserted text ends with \x1b[0m. + state = _active_ansi_state(line, tail_char) + + line = line[:cur_char] + text + state + line[tail_char:] + cur_col += vis_len + cur_char += len(text) + len(state) # state bytes are in the string now + if token.type == "go_right": - # pyrefly: ignore [unsupported-operation] - wanted_i = cur_i + token.value - max_i = printable_len(line) - needed_space = max(wanted_i - max_i, 0) - line += " " * needed_space - cur_i = wanted_i + wanted_col = cur_col + token.value # type: ignore[operator] + needed = max(wanted_col - printable_len(line), 0) + line += " " * needed + cur_col = wanted_col + cur_char = _col_to_char(line, cur_col) + if token.type == "go_left": - # cur_i = 10 - # token.value = 20(wants to go 20 left) - # max_i = 20 - # min_i = 0 - # wanted_i = -10 - - # pyrefly: ignore [unsupported-operation] - wanted_i = cur_i - token.value - min_i = 0 - needed_space = max(wanted_i * -1, 0) # 10 - line = " " * needed_space + line - cur_i = wanted_i + needed_space # 0 + wanted_col = cur_col - token.value # type: ignore[operator] + if wanted_col < 0: + needed = -wanted_col + line = " " * needed + line + cur_col = 0 + cur_char = 0 + else: + cur_col = wanted_col + cur_char = _col_to_char(line, cur_col) if token.type == "go_to_column": - # ANSI ESC[nG -> move to column n (1-based) - wanted_i = max(token.value, 1) - 1 # convert to 0-based - - # extend line if needed - if wanted_i > printable_len(line): - line += " " * (wanted_i - printable_len(line)) + # escape seq is 1-based, convert to 0-based + wanted_col = token.value - 1 # type: ignore[operator] + needed = max(wanted_col - printable_len(line), 0) + if needed: + line += " " * needed + cur_col = wanted_col + cur_char = _col_to_char(line, cur_col) - cur_i = wanted_i - lines.append(line) + # cur_i = wanted_i + result.append(line) - debug_write_str("\n".join(lines)) - print("\n".join(lines)) - return lines + debug_write_str("\n".join(result)) + print("\n".join(result)) + return result diff --git a/src/anifetch/ansi_process2.py b/src/anifetch/ansi_process2.py new file mode 100644 index 0000000..51315af --- /dev/null +++ b/src/anifetch/ansi_process2.py @@ -0,0 +1,182 @@ +from typing import Literal +import re +import regex +from wcwidth import wcwidth, wcswidth +from dataclasses import dataclass +from .utils import debug_write_str + +@dataclass +class Token: + pass + +@dataclass +class TextToken(Token): + text: str + +@dataclass +class SGRToken(Token): # coloring stuff + params: list[int] + +@dataclass +class GoLeftToken(Token): + amount: int + +@dataclass +class GoToColumn(Token): + index: int # 0 based + +@dataclass +class GoRightToken(Token): + amount: int + +GENERAL_ANSI_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") +GENERAL_ANSI_REGEX = regex.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") +COLOR_ANSI_RE = re.compile(r'\x1B\[[0-9;]*m') + +class Cell: + # modifier = ANSI coloring sequences + # text = a piece of text + # this could be an emoji, or just a normal piece of string. + # width = how many cells this would visually occupy + def __init__(self, text:str, modifier:str, width:int = 1): + self.text = text + self.modifier = modifier + self.width = width + +@dataclass +class CellMap: + """2D array of cells.""" + lines: list[list[Cell]] + +def tokenize(raw_lines:list[str]): + """Tokenize raw string, to be later used to create a CellMap""" + tokens_per_line:list[list[Token]] = [] + + for line in raw_lines: + tokens:list[Token] = [] + text_index = 0 + + for match in regex.finditer(GENERAL_ANSI_REGEX, line): + start, end = match.span() + + if start > text_index: + tokens.append(TextToken(line[text_index:start])) + + match_text = line[start:end] + + bracket = match_text.find("[") + # example: \x1b[100D + _left = (bracket + 1) if bracket != -1 else 1 + _right = len(match_text) -1 + + amount_str = match_text[_left:_right] + + code = match_text[-1].upper() + + if code in ("C","D","G"): + try: + amount = int(amount_str) if amount_str else 1 + except ValueError: + amount = 1 + if code == "C": + tokens.append(GoRightToken(amount)) + elif code == "D": + tokens.append(GoLeftToken(amount)) + elif code == "G": + tokens.append(GoToColumn(amount - 1)) # escape seq is 1 based, convert to 0 based + + # TODO: also check for ANSI formatting string bs + if code == "M": + if amount_str: + amount_values = [int(v) for v in amount_str.split(";")] + else: + amount_values = [0] + tokens.append(SGRToken(amount_values)) + + text_index = end + + # check if + if text_index < len(line): + tokens.append(TextToken(line[text_index:])) + + tokens_per_line.append(tokens) + + return tokens_per_line + +def split_to_cells(tokenised_lines:list[list[Token]]): + grapheme_pattern = regex.compile(r"\X") + + cellmap = CellMap([]) + for line_tokens in tokenised_lines: + cells:list[Cell] = [] + current_modifier = "" + cell_index = 0 + for token in line_tokens: + if isinstance(token, TextToken): + graphemes = grapheme_pattern.findall(token.text) + for g in graphemes: + # g: str + width = wcswidth(g) + if width == 1: + cells.insert(cell_index, Cell(g, modifier=current_modifier)) + cell_index += 1 + elif width == 2: + cells.insert(cell_index, Cell(g, modifier=current_modifier, width=2)) + cells.insert(cell_index + 1, Cell("", modifier="")) # continuation cell # maybe +2 instead? + cell_index += 2 + else: + raise Exception(f"Invalid width {width} {g}") + current_modifier = "" + if isinstance(token, SGRToken): + # example: \x1b[1;31m + current_modifier = "\x1b[" + ";".join([str(v) for v in token.params]) + "m" + if isinstance(token, GoRightToken): + wanted_cell_index = cell_index + token.amount + needed = max(wanted_cell_index - len(cells), 0) + for _ in range(needed): + cells.append(Cell(" ", modifier="")) # not sure if this is the correct play but whatever + cell_index = wanted_cell_index + if isinstance(token, GoLeftToken): + wanted_cell_index = cell_index - token.amount + if wanted_cell_index < 0: + needed = -wanted_cell_index + for _ in range(needed): + cells.insert(0, Cell(" ", modifier="")) + cell_index = 0 + else: + cell_index = wanted_cell_index + cellmap.lines.append(cells) + return cellmap + +def expand_ansi_movement_seq2(lines:list[str]): + cellmap = split_to_cells(tokenize(lines)) + out = [] + for line in cellmap.lines: + out_line = "" + for cell in line: + out_line += cell.modifier + cell.text + out.append(out_line) + + debug_write_str("\n".join(out)) + print("\n".join(out)) + return out + + +if __name__ == '__main__': + graphemes:list[str] = regex.findall(r"\X", "🚀\x1b[100D👨‍👩‍👧‍👦é💻") + for g in graphemes: + # g: str + # print(type(g), dir(g)) + print("character:", g) + print("---") + + t = " 💻 " + print(t) + print(len(t)) + print(wcswidth(t)) + print("---") + print(0,t[0]) + print(1,t[1]) + print(2,t[2]) + print("---") + tokenize("lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ") diff --git a/src/anifetch/core.py b/src/anifetch/core.py index 72c5a6a..d5dc884 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -3,6 +3,7 @@ """ from .ansi_process import expand_ansi_movement_seq,strip_ansi_colors +from .ansi_process2 import expand_ansi_movement_seq2 import json import os import pathlib @@ -263,7 +264,9 @@ def run_anifetch(args): not args.neofetch, neofetch_status, args.force, args.config ) # fetch_output = strip_ansi_colors(fetch_output) # if I strip ansi colors the output is nearly the same as fastfetch - expand_ansi_movement_seq(fetch_output) + # expand_ansi_movement_seq(fetch_output) + expand_ansi_movement_seq2(fetch_output) + raise SystemExit # copy fetch_output to fetch_lines diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index 8677b56..29471a7 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -154,6 +154,10 @@ def show_cursor(): def clean_ansi(raw_text: str): return ANSI_RE.sub("", raw_text) +def strip_ansi(text): + ansi_escape = re.compile(r"\x1b\[[0-9;]*m") # color only + return ansi_escape.sub("", text) + def get_character_width(raw: str): """Gives the raw terminal width of a particular string by stripping ANSI codes, removing \n \t \r and using wcwidth to get the actual character width.""" @@ -226,11 +230,6 @@ def normal_print(should_print: bool, *msg): print(*msg) -def strip_ansi(text): - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") - return ansi_escape.sub("", text) - - def get_text_length_of_formatted_text(text: str): text = strip_ansi(text) return len(text) From 16f0bffea794e3ab203f7ac71252a6ca47f007f2 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:50:25 +0300 Subject: [PATCH 13/25] clean up ansi_process.py --- src/anifetch/ansi_process.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index c4adb41..4e000ae 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -41,8 +41,6 @@ def _col_to_char(line: str, col: int) -> int: return i - - class Token: def __init__(self, type_:Literal["text", "go_left", "go_right", "go_to_column", "sgr"], value) -> None: self.type = type_ @@ -57,10 +55,6 @@ def __repr__(self) -> str: return f"<{self.type} {self.value}>" # return f"\x1b[{self.value}{code}" -# How to fix this -# 1 Proper handling of text length via wcwidth(color escape sequences mess with the strings) -# 2 proper handling of emojis. When this 💻 emoji is present in the config, the code prints 1 more border than required - def strip_ansi_colors(lines: list[str]) -> list[str]: ANSI_COLOR_RE = re.compile(r'\x1B\[[0-9;]*m') @@ -87,8 +81,6 @@ def tokenize_lines(lines:list[str]): match_text = line[start:end] - # debug_write_str(f"MATCH.STRING\n\n {match_text}") - bracket = match_text.find("[") # example: \x1b[100D _left = (bracket + 1) if bracket != -1 else 1 @@ -180,6 +172,6 @@ def expand_ansi_movement_seq(lines:list[str]): # cur_i = wanted_i result.append(line) - debug_write_str("\n".join(result)) + # debug_write_str("\n".join(result)) print("\n".join(result)) return result From efd9601ce83751e0fb2dde7f039fcf8404a5654d Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:50:35 +0300 Subject: [PATCH 14/25] update interval info in cli.py --- src/anifetch/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anifetch/cli.py b/src/anifetch/cli.py index 033b90e..8e9f374 100644 --- a/src/anifetch/cli.py +++ b/src/anifetch/cli.py @@ -147,7 +147,7 @@ "--interval", required=False, type=float, - help="Set fetch refresh interval in seconds. Default is -1(never).", + help="Set fetch update interval in seconds. Default is -1(never). ", default=-1, ) parser.add_argument( From 0b7d22c7c54c1ac5885408e242165cd93bb1c2e9 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 14:50:55 +0300 Subject: [PATCH 15/25] get rid of unnecessary variable in core.py --- src/anifetch/core.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/anifetch/core.py b/src/anifetch/core.py index d5dc884..1c44867 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -2,8 +2,8 @@ Anifetch core module for running the animation. """ -from .ansi_process import expand_ansi_movement_seq,strip_ansi_colors -from .ansi_process2 import expand_ansi_movement_seq2 +from .ansi_process import expand_ansi_movement_seq +# from .ansi_process2 import expand_ansi_movement_seq2 import json import os import pathlib @@ -260,17 +260,16 @@ def run_anifetch(args): HEIGHT = args.height # Get the fetch output(neofetch/fastfetch) - fetch_output: list[str] = get_fetch_output( + fetch_lines: list[str] = get_fetch_output( not args.neofetch, neofetch_status, args.force, args.config ) # fetch_output = strip_ansi_colors(fetch_output) # if I strip ansi colors the output is nearly the same as fastfetch - # expand_ansi_movement_seq(fetch_output) - expand_ansi_movement_seq2(fetch_output) + # s = time.perf_counter() + fetch_lines = expand_ansi_movement_seq(fetch_lines) + # e = time.perf_counter() + # print(e-s) + # raise SystemExit - raise SystemExit - - # copy fetch_output to fetch_lines - fetch_lines: list[str] = fetch_output[:] len_fetch = len(fetch_lines) # put cached frames here @@ -375,7 +374,7 @@ def run_anifetch(args): chafa_args, args.center, len_fetch, - fetch_output, + fetch_lines, ) futures.append(future) # if wanted aspect ratio doesnt match source, chafa makes width as high as it can, and adjusts height accordingly. @@ -401,7 +400,7 @@ def run_anifetch(args): pad = (len_chafa - len_fetch) // 2 remind = (len_chafa - len_fetch) % 2 fetch_lines = ( - [" " * WIDTH] * pad + fetch_output + [" " * WIDTH] * (pad + remind) + [" " * WIDTH] * pad + fetch_lines + [" " * WIDTH] * (pad + remind) ) HEIGHT = len(frames[0].splitlines()) @@ -483,6 +482,7 @@ def run_anifetch(args): args.loop, args.cleanup, args.no_key_exit, + args.config, len_chafa or None, WIDTH, GAP, From 21d86e42775120905f620f3ee08eac6364f548db Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:01:46 +0300 Subject: [PATCH 16/25] apply ansi processor to fetch refresh in renderer --- src/anifetch/ansi_process2.py | 54 +++++++++++++++++------------------ src/anifetch/renderer.py | 10 +++++-- src/anifetch/utils.py | 2 +- 3 files changed, 35 insertions(+), 31 deletions(-) diff --git a/src/anifetch/ansi_process2.py b/src/anifetch/ansi_process2.py index 51315af..f6c2f07 100644 --- a/src/anifetch/ansi_process2.py +++ b/src/anifetch/ansi_process2.py @@ -1,10 +1,15 @@ -from typing import Literal import re import regex from wcwidth import wcwidth, wcswidth from dataclasses import dataclass from .utils import debug_write_str +# this is slower but more readable, don't use this. +# this is because im using \X to get graphemes +# creating Cell objects +# wcswidth(maybe?) +# and using regex? I'd assume 're' is faster + @dataclass class Token: pass @@ -92,6 +97,7 @@ def tokenize(raw_lines:list[str]): else: amount_values = [0] tokens.append(SGRToken(amount_values)) + debug_write_str(f"shitty sgr ansi coloring shit {amount_str} \n") text_index = end @@ -118,23 +124,33 @@ def split_to_cells(tokenised_lines:list[list[Token]]): # g: str width = wcswidth(g) if width == 1: - cells.insert(cell_index, Cell(g, modifier=current_modifier)) + # Extend if needed, then overwrite + while len(cells) <= cell_index: + cells.append(Cell(" ", modifier="")) + cells[cell_index] = Cell(g, modifier=current_modifier) cell_index += 1 elif width == 2: - cells.insert(cell_index, Cell(g, modifier=current_modifier, width=2)) - cells.insert(cell_index + 1, Cell("", modifier="")) # continuation cell # maybe +2 instead? + while len(cells) <= cell_index + 1: + cells.append(Cell(" ", modifier="")) + cells[cell_index] = Cell(g, modifier=current_modifier, width=2) + cells[cell_index + 1] = Cell("", modifier="") # continuation cell cell_index += 2 else: raise Exception(f"Invalid width {width} {g}") - current_modifier = "" + # current_modifier = "" # HUHHHH if I just disable it it gets better if isinstance(token, SGRToken): + debug_write_str(f"skibidi skibidi the sgrtoken is here guys {token}\n") # example: \x1b[1;31m - current_modifier = "\x1b[" + ";".join([str(v) for v in token.params]) + "m" + # depending on whether there is '=' or '+=' there the formatting can be fucked + if token.params == [0]: # reset + current_modifier = "\x1b[0m" + else: # add + current_modifier += "\x1b[" + ";".join([str(v) for v in token.params]) + "m" if isinstance(token, GoRightToken): wanted_cell_index = cell_index + token.amount needed = max(wanted_cell_index - len(cells), 0) for _ in range(needed): - cells.append(Cell(" ", modifier="")) # not sure if this is the correct play but whatever + cells.append(Cell(" ", modifier="")) cell_index = wanted_cell_index if isinstance(token, GoLeftToken): wanted_cell_index = cell_index - token.amount @@ -149,6 +165,7 @@ def split_to_cells(tokenised_lines:list[list[Token]]): return cellmap def expand_ansi_movement_seq2(lines:list[str]): + debug_write_str("---------------\n") cellmap = split_to_cells(tokenize(lines)) out = [] for line in cellmap.lines: @@ -156,27 +173,8 @@ def expand_ansi_movement_seq2(lines:list[str]): for cell in line: out_line += cell.modifier + cell.text out.append(out_line) + out[-1] += "\x1b[0m" # reset - debug_write_str("\n".join(out)) + # debug_write_str("\n".join(out)) print("\n".join(out)) return out - - -if __name__ == '__main__': - graphemes:list[str] = regex.findall(r"\X", "🚀\x1b[100D👨‍👩‍👧‍👦é💻") - for g in graphemes: - # g: str - # print(type(g), dir(g)) - print("character:", g) - print("---") - - t = " 💻 " - print(t) - print(len(t)) - print(wcswidth(t)) - print("---") - print(0,t[0]) - print(1,t[1]) - print(2,t[2]) - print("---") - tokenize("lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ") diff --git a/src/anifetch/renderer.py b/src/anifetch/renderer.py index 52cdb10..afb17ab 100644 --- a/src/anifetch/renderer.py +++ b/src/anifetch/renderer.py @@ -10,6 +10,8 @@ clear_screen_soft, get_terminal_width, ) +from .ansi_process import expand_ansi_movement_seq +# from .ansi_process2 import expand_ansi_movement_seq2 import subprocess from .keyreader import KeyReader from typing import Literal @@ -19,8 +21,9 @@ from rich.text import Text from rich.console import Console from rich.align import Align +# import re - +# _CURSOR_MOVE_RE = re.compile(r'\x1b\[[0-9;]*[ABCDGHJKST]') # import logging # logger = logging.getLogger(__name__) # logging.basicConfig( @@ -70,6 +73,7 @@ def __init__( loop: int, cleanup: bool, no_key_exit: bool, + config, len_chafa: int | None, width: int, gap: int, @@ -102,6 +106,7 @@ def __init__( self.loop: int = loop self.cleanup: bool = cleanup self.no_key_exit: bool = no_key_exit + self.config = config self.len_chafa: int | None = len_chafa self.width: int = width self.gap: int = gap @@ -161,8 +166,9 @@ def _(): if self.stop_fetch_thread: return fetch_output: list[str] = get_fetch_output( - self.use_fastfetch, self.neofetch_status, self.force_neofetch + self.use_fastfetch, self.neofetch_status, self.force_neofetch, self.config ) + fetch_output = expand_ansi_movement_seq(fetch_output) if self.stop_fetch_thread: return diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index 29471a7..083469e 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -720,7 +720,7 @@ def printable_len(raw:str): return w if w >= 0 else sum(max(wcwidth.wcwidth(c), 0) for c in cleaned) def debug_write_str(t:str): - with open("debug.ignore", "w", encoding="utf-8") as f: + with open("debug.ignore", "a", encoding="utf-8") as f: f.write(t) def overwrite_string(original_text:str, index:int, text_to_overwrite:str): From 0bcb572b0b6de2d1ab33f2db6a313a9cd8c216ac Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:02:11 +0300 Subject: [PATCH 17/25] fixed #98 --- src/anifetch/renderer.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/anifetch/renderer.py b/src/anifetch/renderer.py index afb17ab..e6cdc69 100644 --- a/src/anifetch/renderer.py +++ b/src/anifetch/renderer.py @@ -23,7 +23,6 @@ from rich.align import Align # import re -# _CURSOR_MOVE_RE = re.compile(r'\x1b\[[0-9;]*[ABCDGHJKST]') # import logging # logger = logging.getLogger(__name__) # logging.basicConfig( From 53704c6c7b7aec3db05f2d9c12545eff83f57a51 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sat, 13 Jun 2026 15:05:40 +0300 Subject: [PATCH 18/25] forgot to format --- src/anifetch/ansi2txt.py | 3 +- src/anifetch/ansi_process.py | 58 ++++++++++++++++--------------- src/anifetch/ansi_process2.py | 65 ++++++++++++++++++++++------------- src/anifetch/core.py | 1 + src/anifetch/renderer.py | 6 +++- src/anifetch/utils.py | 18 +++++++--- 6 files changed, 94 insertions(+), 57 deletions(-) diff --git a/src/anifetch/ansi2txt.py b/src/anifetch/ansi2txt.py index 619916b..f24d43a 100644 --- a/src/anifetch/ansi2txt.py +++ b/src/anifetch/ansi2txt.py @@ -4,6 +4,7 @@ # Original code from https://github.com/mmlb/ansi2txt + def ansi2txt(text: str) -> str: EOF = "" pos = 0 @@ -52,4 +53,4 @@ def getchar(): elif ch != EOF: output.append(ch) - return "".join(output) \ No newline at end of file + return "".join(output) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index 4e000ae..09a9d2d 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -1,14 +1,12 @@ from typing import Literal import re -from .utils import debug_write_str, overwrite_string, printable_len -from .ansi2txt import ansi2txt +from .utils import printable_len import wcwidth - - _ANSI_RE = re.compile(r"(?:\x1B\[|\x9B)[\d;]*[A-Za-z]") + def _active_ansi_state(line: str, up_to: int) -> str: """ Return all ANSI SGR (color/style) sequences active just before Python @@ -18,9 +16,10 @@ def _active_ansi_state(line: str, up_to: int) -> str: return "".join( m.group() for m in _ANSI_RE.finditer(line, 0, up_to) - if m.group()[-1] == "m" # only SGR codes affect color/style + if m.group()[-1] == "m" # only SGR codes affect color/style ) + def _col_to_char(line: str, col: int) -> int: """ Return the Python string index where visual column *col* begins in *line*. @@ -33,34 +32,38 @@ def _col_to_char(line: str, col: int) -> int: break m = _ANSI_RE.match(line, i) if m: - i = m.end() # ANSI sequences are invisible aka: skip, don't count + i = m.end() # ANSI sequences are invisible aka: skip, don't count else: w = wcwidth.wcwidth(line[i]) - c += max(w, 0) # wcwidth returns -1 for non-printable; treat as 0-wide + c += max(w, 0) # wcwidth returns -1 for non-printable; treat as 0-wide i += 1 return i class Token: - def __init__(self, type_:Literal["text", "go_left", "go_right", "go_to_column", "sgr"], value) -> None: + def __init__( + self, + type_: Literal["text", "go_left", "go_right", "go_to_column", "sgr"], + value, + ) -> None: self.type = type_ - self.value: str|int = value - + self.value: str | int = value + def __repr__(self) -> str: if self.type == "text": # pyrefly: ignore [bad-return] return self.value else: - code = "C" if self.type == "go_left" else "D" return f"<{self.type} {self.value}>" # return f"\x1b[{self.value}{code}" def strip_ansi_colors(lines: list[str]) -> list[str]: - ANSI_COLOR_RE = re.compile(r'\x1B\[[0-9;]*m') - return [ANSI_COLOR_RE.sub('', line) for line in lines] + ANSI_COLOR_RE = re.compile(r"\x1B\[[0-9;]*m") + return [ANSI_COLOR_RE.sub("", line) for line in lines] + -def tokenize_lines(lines:list[str]): +def tokenize_lines(lines: list[str]): # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsu]" @@ -80,11 +83,11 @@ def tokenize_lines(lines:list[str]): line_tokens.append(Token("text", line[text_index:start])) match_text = line[start:end] - + bracket = match_text.find("[") # example: \x1b[100D _left = (bracket + 1) if bracket != -1 else 1 - _right = len(match_text) -1 + _right = len(match_text) - 1 amount_str = match_text[_left:_right] @@ -101,12 +104,12 @@ def tokenize_lines(lines:list[str]): line_tokens.append(Token("go_left", amount)) elif code == "G": line_tokens.append(Token("go_to_column", amount)) - - #else: + + # else: # continue - + text_index = end - + # check if there is remaining text after the last escape sequence match. if text_index < len(line): line_tokens.append(Token("text", line[text_index:])) @@ -114,15 +117,16 @@ def tokenize_lines(lines:list[str]): line_tokens_all.append(line_tokens) return line_tokens_all -def expand_ansi_movement_seq(lines:list[str]): + +def expand_ansi_movement_seq(lines: list[str]): # debug_write_str("\n\n\n".join(lines)) line_tokens_all = tokenize_lines(lines) result: list[str] = [] - + for line_tokens in line_tokens_all: line = "" - cur_col = 0 # visual terminal column + cur_col = 0 # visual terminal column cur_char = 0 # string index(raw data) for token in line_tokens: if token.type == "text": @@ -141,8 +145,8 @@ def expand_ansi_movement_seq(lines:list[str]): line = line[:cur_char] + text + state + line[tail_char:] cur_col += vis_len - cur_char += len(text) + len(state) # state bytes are in the string now - + cur_char += len(text) + len(state) # state bytes are in the string now + if token.type == "go_right": wanted_col = cur_col + token.value # type: ignore[operator] needed = max(wanted_col - printable_len(line), 0) @@ -168,10 +172,10 @@ def expand_ansi_movement_seq(lines:list[str]): line += " " * needed cur_col = wanted_col cur_char = _col_to_char(line, cur_col) - + # cur_i = wanted_i result.append(line) - + # debug_write_str("\n".join(result)) print("\n".join(result)) return result diff --git a/src/anifetch/ansi_process2.py b/src/anifetch/ansi_process2.py index f6c2f07..f9caaee 100644 --- a/src/anifetch/ansi_process2.py +++ b/src/anifetch/ansi_process2.py @@ -1,6 +1,6 @@ import re import regex -from wcwidth import wcwidth, wcswidth +from wcwidth import wcswidth from dataclasses import dataclass from .utils import debug_write_str @@ -10,55 +10,66 @@ # wcswidth(maybe?) # and using regex? I'd assume 're' is faster + @dataclass class Token: pass + @dataclass class TextToken(Token): text: str + @dataclass class SGRToken(Token): # coloring stuff params: list[int] + @dataclass class GoLeftToken(Token): amount: int + @dataclass class GoToColumn(Token): index: int # 0 based + @dataclass class GoRightToken(Token): amount: int + GENERAL_ANSI_RE = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") GENERAL_ANSI_REGEX = regex.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") -COLOR_ANSI_RE = re.compile(r'\x1B\[[0-9;]*m') +COLOR_ANSI_RE = re.compile(r"\x1B\[[0-9;]*m") + class Cell: # modifier = ANSI coloring sequences - # text = a piece of text + # text = a piece of text # this could be an emoji, or just a normal piece of string. # width = how many cells this would visually occupy - def __init__(self, text:str, modifier:str, width:int = 1): + def __init__(self, text: str, modifier: str, width: int = 1): self.text = text self.modifier = modifier self.width = width + @dataclass class CellMap: """2D array of cells.""" + lines: list[list[Cell]] -def tokenize(raw_lines:list[str]): + +def tokenize(raw_lines: list[str]): """Tokenize raw string, to be later used to create a CellMap""" - tokens_per_line:list[list[Token]] = [] + tokens_per_line: list[list[Token]] = [] for line in raw_lines: - tokens:list[Token] = [] + tokens: list[Token] = [] text_index = 0 for match in regex.finditer(GENERAL_ANSI_REGEX, line): @@ -72,13 +83,13 @@ def tokenize(raw_lines:list[str]): bracket = match_text.find("[") # example: \x1b[100D _left = (bracket + 1) if bracket != -1 else 1 - _right = len(match_text) -1 + _right = len(match_text) - 1 amount_str = match_text[_left:_right] code = match_text[-1].upper() - if code in ("C","D","G"): + if code in ("C", "D", "G"): try: amount = int(amount_str) if amount_str else 1 except ValueError: @@ -88,33 +99,36 @@ def tokenize(raw_lines:list[str]): elif code == "D": tokens.append(GoLeftToken(amount)) elif code == "G": - tokens.append(GoToColumn(amount - 1)) # escape seq is 1 based, convert to 0 based - + tokens.append( + GoToColumn(amount - 1) + ) # escape seq is 1 based, convert to 0 based + # TODO: also check for ANSI formatting string bs if code == "M": if amount_str: - amount_values = [int(v) for v in amount_str.split(";")] + amount_values = [int(v) for v in amount_str.split(";")] else: amount_values = [0] tokens.append(SGRToken(amount_values)) debug_write_str(f"shitty sgr ansi coloring shit {amount_str} \n") text_index = end - - # check if + + # check if if text_index < len(line): tokens.append(TextToken(line[text_index:])) - + tokens_per_line.append(tokens) return tokens_per_line -def split_to_cells(tokenised_lines:list[list[Token]]): + +def split_to_cells(tokenised_lines: list[list[Token]]): grapheme_pattern = regex.compile(r"\X") - + cellmap = CellMap([]) for line_tokens in tokenised_lines: - cells:list[Cell] = [] + cells: list[Cell] = [] current_modifier = "" cell_index = 0 for token in line_tokens: @@ -132,8 +146,10 @@ def split_to_cells(tokenised_lines:list[list[Token]]): elif width == 2: while len(cells) <= cell_index + 1: cells.append(Cell(" ", modifier="")) - cells[cell_index] = Cell(g, modifier=current_modifier, width=2) - cells[cell_index + 1] = Cell("", modifier="") # continuation cell + cells[cell_index] = Cell(g, modifier=current_modifier, width=2) + cells[cell_index + 1] = Cell( + "", modifier="" + ) # continuation cell cell_index += 2 else: raise Exception(f"Invalid width {width} {g}") @@ -145,7 +161,9 @@ def split_to_cells(tokenised_lines:list[list[Token]]): if token.params == [0]: # reset current_modifier = "\x1b[0m" else: # add - current_modifier += "\x1b[" + ";".join([str(v) for v in token.params]) + "m" + current_modifier += ( + "\x1b[" + ";".join([str(v) for v in token.params]) + "m" + ) if isinstance(token, GoRightToken): wanted_cell_index = cell_index + token.amount needed = max(wanted_cell_index - len(cells), 0) @@ -164,7 +182,8 @@ def split_to_cells(tokenised_lines:list[list[Token]]): cellmap.lines.append(cells) return cellmap -def expand_ansi_movement_seq2(lines:list[str]): + +def expand_ansi_movement_seq2(lines: list[str]): debug_write_str("---------------\n") cellmap = split_to_cells(tokenize(lines)) out = [] @@ -174,7 +193,7 @@ def expand_ansi_movement_seq2(lines:list[str]): out_line += cell.modifier + cell.text out.append(out_line) out[-1] += "\x1b[0m" # reset - + # debug_write_str("\n".join(out)) print("\n".join(out)) return out diff --git a/src/anifetch/core.py b/src/anifetch/core.py index 1c44867..fe4976a 100644 --- a/src/anifetch/core.py +++ b/src/anifetch/core.py @@ -3,6 +3,7 @@ """ from .ansi_process import expand_ansi_movement_seq + # from .ansi_process2 import expand_ansi_movement_seq2 import json import os diff --git a/src/anifetch/renderer.py b/src/anifetch/renderer.py index e6cdc69..569971d 100644 --- a/src/anifetch/renderer.py +++ b/src/anifetch/renderer.py @@ -11,6 +11,7 @@ get_terminal_width, ) from .ansi_process import expand_ansi_movement_seq + # from .ansi_process2 import expand_ansi_movement_seq2 import subprocess from .keyreader import KeyReader @@ -165,7 +166,10 @@ def _(): if self.stop_fetch_thread: return fetch_output: list[str] = get_fetch_output( - self.use_fastfetch, self.neofetch_status, self.force_neofetch, self.config + self.use_fastfetch, + self.neofetch_status, + self.force_neofetch, + self.config, ) fetch_output = expand_ansi_movement_seq(fetch_output) diff --git a/src/anifetch/utils.py b/src/anifetch/utils.py index 083469e..95b57a4 100644 --- a/src/anifetch/utils.py +++ b/src/anifetch/utils.py @@ -154,8 +154,9 @@ def show_cursor(): def clean_ansi(raw_text: str): return ANSI_RE.sub("", raw_text) + def strip_ansi(text): - ansi_escape = re.compile(r"\x1b\[[0-9;]*m") # color only + ansi_escape = re.compile(r"\x1b\[[0-9;]*m") # color only return ansi_escape.sub("", text) @@ -711,7 +712,8 @@ def split_to_frames(args, CACHE_PATH, IS_TRANSPARENT, stdout, stderr): text=True, ) -def printable_len(raw:str): + +def printable_len(raw: str): """Returns printable length of the string.""" cleaned = clean_ansi(raw) w = wcwidth.wcswidth(cleaned) @@ -719,11 +721,17 @@ def printable_len(raw:str): # it can return -1, so if it does return -1 do per character return w if w >= 0 else sum(max(wcwidth.wcwidth(c), 0) for c in cleaned) -def debug_write_str(t:str): + +def debug_write_str(t: str): with open("debug.ignore", "a", encoding="utf-8") as f: f.write(t) -def overwrite_string(original_text:str, index:int, text_to_overwrite:str): - out = original_text[:index] + text_to_overwrite + original_text[index + printable_len(text_to_overwrite):] + +def overwrite_string(original_text: str, index: int, text_to_overwrite: str): + out = ( + original_text[:index] + + text_to_overwrite + + original_text[index + printable_len(text_to_overwrite) :] + ) return out From d5f292e25e2b49ff9ab0f6483212c36724777865 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:12:02 +0300 Subject: [PATCH 19/25] Update ansi_process.py --- src/anifetch/ansi_process.py | 103 +++++++++++++++++++++++++---------- 1 file changed, 74 insertions(+), 29 deletions(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index 09a9d2d..4aad086 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -35,7 +35,7 @@ def _col_to_char(line: str, col: int) -> int: i = m.end() # ANSI sequences are invisible aka: skip, don't count else: w = wcwidth.wcwidth(line[i]) - c += max(w, 0) # wcwidth returns -1 for non-printable; treat as 0-wide + c += max(w, 0) # wcwidth returns -1 for non-printable i += 1 return i @@ -43,7 +43,16 @@ def _col_to_char(line: str, col: int) -> int: class Token: def __init__( self, - type_: Literal["text", "go_left", "go_right", "go_to_column", "sgr"], + type_: Literal[ + "text", + "go_left", + "go_right", + "go_to_column", + "sgr", + "save_cursor", + "restore_cursor", + "erase_line", + ], value, ) -> None: self.type = type_ @@ -55,7 +64,6 @@ def __repr__(self) -> str: return self.value else: return f"<{self.type} {self.value}>" - # return f"\x1b[{self.value}{code}" def strip_ansi_colors(lines: list[str]) -> list[str]: @@ -66,8 +74,8 @@ def strip_ansi_colors(lines: list[str]) -> list[str]: def tokenize_lines(lines: list[str]): # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" - pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsu]" # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsum]" + pattern = r"\r|(?:\x1B\[|\x9B)[\d;]*[A-GHKfhsum]" line_tokens_all: list[list[Token]] = [] @@ -84,33 +92,47 @@ def tokenize_lines(lines: list[str]): match_text = line[start:end] + if match_text == "\r": + line_tokens.append(Token("go_to_column", 1)) + text_index = end + continue + bracket = match_text.find("[") # example: \x1b[100D _left = (bracket + 1) if bracket != -1 else 1 _right = len(match_text) - 1 - amount_str = match_text[_left:_right] + params_str = match_text[_left:_right] - try: - amount = int(amount_str) if amount_str else 1 - except ValueError: - amount = 1 + # parse multi-param sequences (e.g. \x1b[5;10H → [5, 10]) + if params_str: + params = [int(x) if x else 1 for x in params_str.split(";")] + else: + params = [1] code = match_text[-1] if code == "C": - line_tokens.append(Token("go_right", amount)) + line_tokens.append(Token("go_right", params[0])) elif code == "D": - line_tokens.append(Token("go_left", amount)) + line_tokens.append(Token("go_left", params[0])) elif code == "G": - line_tokens.append(Token("go_to_column", amount)) - - # else: - # continue + line_tokens.append(Token("go_to_column", params[0])) + elif code == "m": + # pass SGR through as a raw token so colours survive reconstruction + line_tokens.append(Token("sgr", match_text)) + elif code == "s": + line_tokens.append(Token("save_cursor", 0)) + elif code == "u": + line_tokens.append(Token("restore_cursor", 0)) + elif code == "K": + # \x1b[K and \x1b[0K both mean "clear to end of line" + amount = int(params_str) if params_str.isdigit() else 0 + line_tokens.append(Token("erase_line", amount)) text_index = end - # check if there is remaining text after the last escape sequence match. + # remaining text after the last escape sequence if text_index < len(line): line_tokens.append(Token("text", line[text_index:])) @@ -119,15 +141,16 @@ def tokenize_lines(lines: list[str]): def expand_ansi_movement_seq(lines: list[str]): - # debug_write_str("\n\n\n".join(lines)) line_tokens_all = tokenize_lines(lines) result: list[str] = [] for line_tokens in line_tokens_all: line = "" - cur_col = 0 # visual terminal column - cur_char = 0 # string index(raw data) + cur_col = 0 # visual terminal column + cur_char = 0 # string index into `line` + saved_col = 0 # for \x1b[s / \x1b[u + for token in line_tokens: if token.type == "text": text: str = token.value # type: ignore[assignment] @@ -138,23 +161,23 @@ def expand_ansi_movement_seq(lines: list[str]): tail_char = _col_to_char(line, cur_col + vis_len) - # Collect the SGR state active at tail_char so that border characters - # in the tail (spaces, ║) keep their original color even when the - # inserted text ends with \x1b[0m. + # Collect the SGR state active at tail_char so that border + # characters in the tail keep their original colour even when + # the inserted text ends with \x1b[0m. state = _active_ansi_state(line, tail_char) line = line[:cur_char] + text + state + line[tail_char:] cur_col += vis_len - cur_char += len(text) + len(state) # state bytes are in the string now + cur_char += len(text) + len(state) - if token.type == "go_right": + elif token.type == "go_right": wanted_col = cur_col + token.value # type: ignore[operator] needed = max(wanted_col - printable_len(line), 0) line += " " * needed cur_col = wanted_col cur_char = _col_to_char(line, cur_col) - if token.type == "go_left": + elif token.type == "go_left": wanted_col = cur_col - token.value # type: ignore[operator] if wanted_col < 0: needed = -wanted_col @@ -164,7 +187,8 @@ def expand_ansi_movement_seq(lines: list[str]): else: cur_col = wanted_col cur_char = _col_to_char(line, cur_col) - if token.type == "go_to_column": + + elif token.type == "go_to_column": # escape seq is 1-based, convert to 0-based wanted_col = token.value - 1 # type: ignore[operator] needed = max(wanted_col - printable_len(line), 0) @@ -173,9 +197,30 @@ def expand_ansi_movement_seq(lines: list[str]): cur_col = wanted_col cur_char = _col_to_char(line, cur_col) - # cur_i = wanted_i + elif token.type == "sgr": + raw: str = token.value # type: ignore[assignment] + line = line[:cur_char] + raw + line[cur_char:] + cur_char += len(raw) + + elif token.type == "save_cursor": + saved_col = cur_col + + elif token.type == "restore_cursor": + cur_col = saved_col + cur_char = _col_to_char(line, cur_col) + + elif token.type == "erase_line": + amount = token.value + if amount == 0: + line = line[:cur_char] + elif amount == 1: + line = " " * cur_char + line[cur_char:] + elif amount == 2: + line = "" + cur_col = 0 + cur_char = 0 + result.append(line) - # debug_write_str("\n".join(result)) print("\n".join(result)) - return result + return result \ No newline at end of file From b5398447692fafe4f7bdc23f35037640a0af803e Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 13:12:51 +0300 Subject: [PATCH 20/25] Update ansi_process.py --- src/anifetch/ansi_process.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index 4aad086..93f968c 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -147,9 +147,9 @@ def expand_ansi_movement_seq(lines: list[str]): for line_tokens in line_tokens_all: line = "" - cur_col = 0 # visual terminal column + cur_col = 0 # visual terminal column cur_char = 0 # string index into `line` - saved_col = 0 # for \x1b[s / \x1b[u + saved_col = 0 # for \x1b[s / \x1b[u for token in line_tokens: if token.type == "text": @@ -223,4 +223,4 @@ def expand_ansi_movement_seq(lines: list[str]): result.append(line) print("\n".join(result)) - return result \ No newline at end of file + return result From cb3a52b6b1f7b5a47ca38cfac85e00f35fbe35a7 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 16:47:36 +0300 Subject: [PATCH 21/25] finally done --- src/anifetch/ansi_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index 93f968c..db420b6 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -75,7 +75,7 @@ def tokenize_lines(lines: list[str]): # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsum]" - pattern = r"\r|(?:\x1B\[|\x9B)[\d;]*[A-GHKfhsum]" + pattern = r"\r|(?:\x1B\[|\x9B)[\d;]*[A-GHKfhsu]" line_tokens_all: list[list[Token]] = [] From d5c0f9c85989009e205e8db7905429fa8c50a863 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 17:33:52 +0300 Subject: [PATCH 22/25] Update ansi_process.py --- src/anifetch/ansi_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index db420b6..de06eda 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -222,5 +222,5 @@ def expand_ansi_movement_seq(lines: list[str]): result.append(line) - print("\n".join(result)) + # print("\n".join(result)) return result From f60327e556e888dd5110c3d3ffb7cb7d5bb4df91 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:51:05 +0300 Subject: [PATCH 23/25] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 6310b52..d9e6a7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ "wcwidth==0.2.14", "rich==14.3.1", "pynput==1.8.1", - "regex==2026.5.9" + # "regex==2026.5.9" ] [project.urls] From 967eb0cb75a62a57e774d6010ea5d87c66c99e50 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:53:25 +0300 Subject: [PATCH 24/25] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index bbfe33a..c7e7e86 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,8 @@ Also, ffmpeg can generate the the same image for 2 consecutive frames, which may Currently only the `symbols` format of chafa is supported, formats like kitty, iterm etc. are not supported. If you try to tell chafa to use iterm, kitty etc. it will just override your format with `symbols` mode. +Most fastfetch configs/presets should work out of the box. If you run into an issue with a particular preset, just open an issue and paste your config. + ## What's Next - [ ] Support different formats like iterm, kitty, sixel etc. From 56e63ed94bf9283b3ee24e5a35721be46a1ee3f1 Mon Sep 17 00:00:00 2001 From: Notenlish <71970100+Notenlish@users.noreply.github.com> Date: Sun, 14 Jun 2026 18:53:58 +0300 Subject: [PATCH 25/25] Update ansi_process.py --- src/anifetch/ansi_process.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/anifetch/ansi_process.py b/src/anifetch/ansi_process.py index de06eda..758bf91 100644 --- a/src/anifetch/ansi_process.py +++ b/src/anifetch/ansi_process.py @@ -75,7 +75,7 @@ def tokenize_lines(lines: list[str]): # lines = ["lllllll lllllll╔════════════════════════════════════════════════════════════════════════════════════════════════════╗ root@debian 💻 ","║║ kernel > 6.12.1"] # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-FfHhsu]" # pattern = r"(?:\x1B\[|\x9B)\d*(?:;\d*)*[A-GHfhsum]" - pattern = r"\r|(?:\x1B\[|\x9B)[\d;]*[A-GHKfhsu]" + pattern = r"\r|(?:\x1B\[|\x9B)[\d;]*[A-GHKfhsu]" # don't include 'm' in this line_tokens_all: list[list[Token]] = []