Практика Исправления Кодировок В Терминале Audion GUI
Этот документ фиксирует решения, которые шаблон должен переносить в новые GUI-проекты, чтобы терминальное окно нормально показывало вывод WinGet, PowerShell, CMD, Python и других Windows CLI.
Практика родилась на канарейке Audion Winget, где нужно было убрать кракозябры в русском выводе WinGet, сохранить progress bar, не сломать псевдографику и оставить ANSI-color.
Проблема
Встроенный терминал GUI получает вывод дочерних процессов через pipe, а не как обычное консольное окно Windows. Из-за этого:
- русский текст WinGet может превращаться в
╨,╤,Рџ,СЂи похожий мусор; - табличная псевдографика WinGet и progress bar могут отображаться неправильно;
- spinner-анимация
- \ | /может засорять лог; - одна глобальная настройка UTF-8 может ломать утилиты, которые реально пишут в OEM/ANSI-кодировке;
- ANSI-color можно случайно вырезать раньше HTML-рендера.
Главный Принцип
Не доверять одной фиксированной кодировке для всего вывода Windows CLI.
WinGet, PowerShell, CMD, Python и старые консольные утилиты могут отдавать разные байты в зависимости от версии Windows, локали, способа запуска и того, считают ли они stdout настоящей консолью или pipe.
Правильная цепочка:
- Читать внешний процесс как
bytes, где это возможно. - Декодировать каждую порцию вывода через общий эвристический декодер.
- Не удалять ANSI escape-последовательности на этапе декодирования.
- Удалять только чистую spinner-анимацию, но не progress bar и не псевдографику.
- Рендерить ANSI в HTML уже на стороне GUI.
Общий Декодер Вывода
Шаблонный файл:
system_core/core/output_decode.py
Ключевые идеи:
- пробуются несколько кодировок:
utf-8,utf-16,cp866, системная locale encoding,mbcs,cp1251; - каждая расшифровка получает score;
- русский текст повышает score;
- нормальная терминальная графика Unicode повышает score;
- признаки mojibake (
Р°,СЃ,╨,╤,…,тАи похожие) понижают score; - побеждает кандидат с лучшим score;
- если строгая декодировка не сработала нигде, fallback:
utf-8сerrors="replace".
Важно: декодер не должен удалять ANSI. ANSI escape-коды должны пройти дальше, чтобы GUI мог раскрасить вывод.
Запуск Процессов
Базовая функция utf8_subprocess_env в system_core/core/jobs.py задаёт:
PYTHONUTF8=1
PYTHONIOENCODING=utf-8
PYTHONUNBUFFERED=1
Для CLI-процессов, где нужен цветной вывод, добавляйте:
env = utf8_subprocess_env(
{
"AUDION_DISABLE_FZF": "1",
"AUDION_GUI_TERMINAL": "1",
"CLICOLOR": "1",
"CLICOLOR_FORCE": "1",
"FORCE_COLOR": "1",
}
)
env.pop("NO_COLOR", None)
Это помогает Python-процессам и современным CLI, но не заменяет декодер. WinGet и системные утилиты всё равно нужно читать осторожно.
Почему Внешние CLI Лучше Читать Как Bytes
Для WinGet и большинства внешних команд лучше не включать text=True в subprocess.Popen.
Причина: Python тогда декодирует stdout сам, до эвристического декодера. Если Python выбрал не ту кодировку, GUI получит уже испорченный текст.
Рабочий шаблон:
- обычные внешние процессы читаются как bytes;
- каждая порция stdout/stderr проходит через
decode_process_bytes; - только собственные Python-команды можно запускать с
text=True, encoding="utf-8", errors="replace", если контролируетсяPYTHONIOENCODING=utf-8.
Обработка Carriage Return И Spinner
WinGet активно использует \r для progress и spinner.
Практика:
text = text.replace("\r\n", "\n").replace("\r", "\n")
Затем чистые spinner-строки можно отбрасывать:
SPINNER_FRAME_CHARS = set("-\\|/ \t")
Отбрасывать можно только строки, которые состоят исключительно из символов spinner. Нельзя выкидывать строки с блоками █, ▒, табличной графикой или текстом, иначе потеряется полезный progress WinGet.
ANSI-Color
ANSI должен сохраняться до HTML-render.
Шаблонный renderer:
system_core/core/ansi.py
Что он делает:
- понимает SGR-коды вроде
\x1b[31m,\x1b[33m,\x1b[0m; - переводит foreground/background color в HTML
<span style="...">; - поддерживает bold/dim;
- пропускает обычный текст через HTML escaping;
- умеет strip ANSI отдельно, но терминальный GUI должен использовать HTML-render, а не plain strip.
Практическая проверка:
- строка с
\x1b[31mпроходит через decode без удаления escape-кода; - затем
AnsiHtmlRendererпревращает её в HTML со стилем цвета.
PowerShell И CMD
Для пользовательского терминала GUI:
- PowerShell получает UTF-8 preamble;
- CMD запускается через
chcp 65001 >nul; - для PowerShell включается ANSI rendering, если доступен
$PSStyle.
Это снижает шанс неверного вывода, но не отменяет общий декодер, потому что WinGet и сторонние CLI могут вести себя иначе.
Что Переносить В Новые Проекты
Минимальный набор:
- Скопировать или оставить шаблонный
system_core/core/output_decode.py. - Использовать
decode_process_bytesдля stdout/stderr внешних процессов. - Читать WinGet/CLI как bytes, не как
text=True. - Оставить
text=True, encoding="utf-8"только для своих Python-процессов, где контролируетсяPYTHONIOENCODING=utf-8. - В окружение процесса добавить
PYTHONUTF8,PYTHONIOENCODING,PYTHONUNBUFFERED. - Не задавать
NO_COLOR; для цветного вывода использоватьCLICOLOR_FORCE=1иFORCE_COLOR=1. \rпереводить в\n.- Отфильтровывать только spinner-only строки.
- ANSI рендерить отдельным HTML renderer, а не удалять на входе.
- Проверить три сценария: русский CLI-текст, progress bar, ANSI-color.
Чего Не Делать
- Не лечить всё одним
encoding="utf-8"вPopen. - Не лечить всё одним
encoding="cp866". - Не вырезать всю псевдографику как мусор.
- Не удалять ANSI escape-коды до HTML-render.
- Не считать
\rмусором целиком: он часто несёт progress-состояния. - Не полагаться только на
chcp 65001: для pipe-вывода это не универсальная гарантия.
Быстрый Smoke-Тест
Для проверки нового проекта достаточно прогнать в терминале GUI команды, которые дают разные типы вывода:
Write-Output 'Найдено 7-Zip [7zip.7zip] Версия 26.01'
Write-Output 'Скачивание https://7-zip.org/a/7z2601-x64.msi'
Write-Output '████████████████ 1.90 MB / 1.90 MB'
Write-Output '┌──────────┬────────────┬─────────┐'
Write-Output '│ Name │ ID │ Version │'
Write-Output '├──────────┼────────────┼─────────┤'
Write-Output '│ 7-Zip │ 7zip.7zip │ 26.01 │'
Write-Output '└──────────┴────────────┴─────────┘'
Write-Output "`e[33mЖёлтое ANSI-предупреждение`e[0m"
Ожидаемый результат:
- русский текст читаемый;
- таблица не превращается в
╨╤; - progress bar остаётся видимым;
- ANSI-предупреждение окрашивается;
- лишние spinner-строки не засоряют лог.
Итог
Правка кодировок оказалась не одной настройкой, а цепочкой:
bytes stdout -> эвристический decode -> аккуратная обработка \r -> фильтр spinner -> сохранение ANSI -> HTML-render
Именно эта цепочка дала стабильный вывод WinGet в GUI и не сломала ANSI-color.