diff --git a/korman/exporter/logger.py b/korman/exporter/logger.py index 8118d74..9089dee 100644 --- a/korman/exporter/logger.py +++ b/korman/exporter/logger.py @@ -13,7 +13,7 @@ # You should have received a copy of the GNU General Public License # along with Korman. If not, see . -from ..korlib import ConsoleToggler +from ..korlib import ConsoleCursor, ConsoleToggler from .explosions import NonfatalExportError from pathlib import Path import threading @@ -142,6 +142,7 @@ class ExportProgressLogger(_ExportLogger): # because it is difficult to inspect the progress of Blender's internal operators. The best # solution here is to move printing into a thread that can detect long-running ops and display # something visible such as a moving elipsis + self._cursor = ConsoleCursor() self._thread = threading.Thread(target=self._progress_thread) self._queued_lines = [] self._print_condition = threading.Condition() @@ -283,6 +284,8 @@ class ExportProgressLogger(_ExportLogger): def _progress_thread(self): num_dots = 0 + self._cursor.update() + while self._progress_alive: with self._print_condition: signalled = self._print_condition.wait(timeout=1.0) @@ -290,12 +293,16 @@ class ExportProgressLogger(_ExportLogger): # First, we need to print out any queued whole lines. # NOTE: no need to lock anything here as Blender uses CPython (GIL) - if self._queued_lines: - print(*self._queued_lines, sep='\n') - self._queued_lines.clear() + with self._cursor: + if self._queued_lines: + print(*self._queued_lines, sep='\n') + self._queued_lines.clear() # Now, we need to print out the current volatile line, if any. if self._volatile_line: + # On Windows, if we clear the line, the volatile line is nuked as well. + # Probably a race condition in the Win32 console host. + self._cursor.reset() print(self._volatile_line, end="") # If the proc is long running, let us display some elipses so as to not alarm the user @@ -305,6 +312,7 @@ class ExportProgressLogger(_ExportLogger): else: num_dots = 0 print('.' * num_dots, end=" " * (_MAX_ELIPSES - num_dots)) + self._cursor.update() def _progress_get_current(self): return self._step_progress diff --git a/korman/korlib/__init__.py b/korman/korlib/__init__.py index 78a0fe9..70f717b 100644 --- a/korman/korlib/__init__.py +++ b/korman/korlib/__init__.py @@ -72,7 +72,7 @@ else: from _korlib import * finally: - from .console import ConsoleToggler + from .console import ConsoleCursor, ConsoleToggler from .python import * from .texture import TEX_DETAIL_ALPHA, TEX_DETAIL_ADD, TEX_DETAIL_MULTIPLY diff --git a/korman/korlib/console.py b/korman/korlib/console.py index d84106e..f771b68 100644 --- a/korman/korlib/console.py +++ b/korman/korlib/console.py @@ -18,6 +18,81 @@ import ctypes import math import sys +if sys.platform == "win32": + class _Coord(ctypes.Structure): + _fields_ = [("x", ctypes.c_short), + ("y", ctypes.c_short)] + class _SmallRect(ctypes.Structure): + _fields_ = [("Left", ctypes.c_short), + ("Top", ctypes.c_short), + ("Right", ctypes.c_short), + ("Bottom", ctypes.c_short),] + class _ConsoleScreenBufferInfo(ctypes.Structure): + _fields_ = [("dwSize", _Coord), + ("dwCursorPosition", _Coord), + ("wAttributes", ctypes.c_ushort), + ("srWindow", _SmallRect), + ("dwMaximumWindowSize", _Coord)] + + class _ConsoleCursor: + def __init__(self): + self._handle = ctypes.windll.kernel32.GetStdHandle(-11) + self.position = _Coord(0, 0) + + def __del__(self): + ctypes.windll.kernel32.CloseHandle(self._handle) + + @property + def _screen_buffer_info(self): + info = _ConsoleScreenBufferInfo() + ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self._handle, ctypes.pointer(info)) + return info + + def clear(self): + info = self._screen_buffer_info + curPos = info.dwCursorPosition + num_cols = curPos.y - self.position.y + num_rows = curPos.x - self.position.x + num_chars = (info.dwSize.x * num_cols) + num_rows + if num_chars: + nWrite = ctypes.c_ulong() + empty_char = ctypes.c_char(b' ') + ctypes.windll.kernel32.FillConsoleOutputCharacterA(self._handle, empty_char, + num_chars, self.position, + ctypes.pointer(nWrite)) + + def reset(self): + ctypes.windll.kernel32.SetConsoleCursorPosition(self._handle, self.position) + + def update(self): + info = _ConsoleScreenBufferInfo() + ctypes.windll.kernel32.GetConsoleScreenBufferInfo(self._handle, ctypes.pointer(info)) + self.position = info.dwCursorPosition +else: + class _ConsoleCursor: + def clear(self): + # Only clears the current line, unfortunately. + sys.stdout.write("\x1B[K") + + def reset(self): + # Forcibly clears the line after resetting, just in case more + # than one junk line was printed from somewhere else. + sys.stdout.write("\x1B[u\x1B[K") + + def update(self): + sys.stdout.write("\x1B[s") + + +class ConsoleCursor(_ConsoleCursor): + def __enter__(self): + self.clear() + self.reset() + return self + + def __exit__(self, type, value, traceback): + self.update() + + class ConsoleToggler: _instance = None