Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/kimi_cli/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,37 @@
from pathlib import Path


def _ensure_utf8_stdio() -> None:
"""Use UTF-8 for the standard Windows text streams when they support it.

Git Bash may expose the Windows legacy code page (for example, cp936) to
Python. The interactive UI contains characters outside those code pages,
so Rich can otherwise fail while rendering the welcome panel. Redirected
and non-standard streams are deliberately left alone.
"""
if sys.platform != "win32":
return

for stream_name in ("stdout", "stderr"):
stream = getattr(sys, stream_name)
reconfigure = getattr(stream, "reconfigure", None)
if not callable(reconfigure):
continue
try:
reconfigure(encoding="utf-8", errors="replace")
except (OSError, TypeError, ValueError):
# Some embedders expose text-like streams with a partial or locked
# reconfigure implementation. Preserve those streams as-is.
continue


def _prog_name() -> str:
return Path(sys.argv[0]).name or "kimi"


def main(argv: Sequence[str] | None = None) -> int | str | None:
_ensure_utf8_stdio()

from kimi_cli.telemetry.crash import install_crash_handlers, set_phase
from kimi_cli.utils.proxy import normalize_proxy_env

Expand Down
75 changes: 75 additions & 0 deletions tests/core/test_windows_stdio.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from __future__ import annotations

import io
import sys


class _EncodinglessStream:
encoding = None

def __init__(self) -> None:
self.value = ""

def write(self, value: str) -> int:
self.value += value
return len(value)

def flush(self) -> None:
pass


def test_windows_stdio_allows_welcome_rendering_with_cp936_streams(monkeypatch) -> None:
from kimi_cli.__main__ import _ensure_utf8_stdio

stdout_buffer = io.BytesIO()
stderr_buffer = io.BytesIO()
stdout = io.TextIOWrapper(stdout_buffer, encoding="cp936", errors="strict")
stderr = io.TextIOWrapper(stderr_buffer, encoding="cp936", errors="strict")
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(sys, "stdout", stdout)
monkeypatch.setattr(sys, "stderr", stderr)

_ensure_utf8_stdio()

import kimi_cli.ui.shell as shell_ui
from kimi_cli.ui.shell import _print_welcome_info
from kimi_cli.ui.shell.console import _KimiConsole

monkeypatch.setattr(shell_ui, "console", _KimiConsole(file=sys.stdout, highlight=False))
_print_welcome_info("Kimi Code CLI", [])
sys.stdout.flush()

assert sys.stdout.encoding == "utf-8"
assert "Welcome to Kimi Code CLI!" in stdout_buffer.getvalue().decode("utf-8")


def test_windows_stdio_preserves_redirected_and_encodingless_streams(monkeypatch) -> None:
from kimi_cli.__main__ import _ensure_utf8_stdio

redirected = io.StringIO()
encodingless = _EncodinglessStream()
monkeypatch.setattr(sys, "platform", "win32")
monkeypatch.setattr(sys, "stdout", redirected)
monkeypatch.setattr(sys, "stderr", encodingless)

_ensure_utf8_stdio()

assert sys.stdout is redirected
assert sys.stderr is encodingless
print("redirected")
print("encodingless", file=sys.stderr)
assert redirected.getvalue() == "redirected\n"
assert encodingless.value == "encodingless\n"


def test_non_windows_stdio_is_unchanged(monkeypatch) -> None:
from kimi_cli.__main__ import _ensure_utf8_stdio

stream = io.TextIOWrapper(io.BytesIO(), encoding="cp936", errors="strict")
monkeypatch.setattr(sys, "platform", "linux")
monkeypatch.setattr(sys, "stdout", stream)

_ensure_utf8_stdio()

assert sys.stdout is stream
assert sys.stdout.encoding == "cp936"
Loading