summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cs2pov/cli.py197
-rw-r--r--cs2pov/loading.py105
2 files changed, 186 insertions, 116 deletions
diff --git a/cs2pov/cli.py b/cs2pov/cli.py
index f307032..4a55dfc 100644
--- a/cs2pov/cli.py
+++ b/cs2pov/cli.py
@@ -850,39 +850,40 @@ def cmd_pov(args) -> int:
output_path = args.output.resolve()
- # Record
- result = record_demo(
- demo_path=demo_path,
- player_identifier=args.player,
- output_path=output_path,
- resolution=resolution,
- framerate=args.framerate,
- hide_hud=args.no_hud,
- display_num=args.display,
- verbose=args.verbose,
- cs2_path_override=args.cs2_path,
- enable_audio=not args.no_audio,
- audio_device=args.audio_device,
- )
-
- # Post-process
- if result.success and not args.no_trim:
- postprocess_video(
- video_path=result.video_path,
- console_log_path=result.console_log_path,
- player_slot=result.player_slot,
- recording_start_time=result.recording_start_time,
+ with LoadingAnimation():
+ # Record
+ result = record_demo(
+ demo_path=demo_path,
+ player_identifier=args.player,
+ output_path=output_path,
+ resolution=resolution,
+ framerate=args.framerate,
+ hide_hud=args.no_hud,
+ display_num=args.display,
verbose=args.verbose,
- timeline=result.timeline,
+ cs2_path_override=args.cs2_path,
+ enable_audio=not args.no_audio,
+ audio_device=args.audio_device,
)
- elif result.success:
- size_mb = result.video_path.stat().st_size / (1024 * 1024)
- print(f"\nRecording saved: {result.video_path} ({size_mb:.1f} MB)")
- else:
- print(f"\nRecording ended with: {result.exit_reason}")
- if result.video_path.exists():
+
+ # Post-process
+ if result.success and not args.no_trim:
+ postprocess_video(
+ video_path=result.video_path,
+ console_log_path=result.console_log_path,
+ player_slot=result.player_slot,
+ recording_start_time=result.recording_start_time,
+ verbose=args.verbose,
+ timeline=result.timeline,
+ )
+ elif result.success:
size_mb = result.video_path.stat().st_size / (1024 * 1024)
- print(f"Partial recording available: {result.video_path} ({size_mb:.1f} MB)")
+ print(f"\nRecording saved: {result.video_path} ({size_mb:.1f} MB)")
+ else:
+ print(f"\nRecording ended with: {result.exit_reason}")
+ if result.video_path.exists():
+ size_mb = result.video_path.stat().st_size / (1024 * 1024)
+ print(f"Partial recording available: {result.video_path} ({size_mb:.1f} MB)")
return 0 if result.success else 1
@@ -911,31 +912,32 @@ def cmd_record(args) -> int:
output_path = args.output.resolve()
- # Record only
- result = record_demo(
- demo_path=demo_path,
- player_identifier=args.player,
- output_path=output_path,
- resolution=resolution,
- framerate=args.framerate,
- hide_hud=args.no_hud,
- display_num=args.display,
- verbose=args.verbose,
- cs2_path_override=args.cs2_path,
- enable_audio=not args.no_audio,
- audio_device=args.audio_device,
- )
+ with LoadingAnimation():
+ # Record only
+ result = record_demo(
+ demo_path=demo_path,
+ player_identifier=args.player,
+ output_path=output_path,
+ resolution=resolution,
+ framerate=args.framerate,
+ hide_hud=args.no_hud,
+ display_num=args.display,
+ verbose=args.verbose,
+ cs2_path_override=args.cs2_path,
+ enable_audio=not args.no_audio,
+ audio_device=args.audio_device,
+ )
- if result.success:
- size_mb = result.video_path.stat().st_size / (1024 * 1024)
- print(f"\nRaw recording saved: {result.video_path} ({size_mb:.1f} MB)")
- print(f"\nTo trim later, run:")
- print(f" cs2pov trim \"{result.video_path}\" -d \"{demo_path}\" -p \"{args.player}\"")
- else:
- print(f"\nRecording ended with: {result.exit_reason}")
- if result.video_path.exists():
+ if result.success:
size_mb = result.video_path.stat().st_size / (1024 * 1024)
- print(f"Partial recording available: {result.video_path} ({size_mb:.1f} MB)")
+ print(f"\nRaw recording saved: {result.video_path} ({size_mb:.1f} MB)")
+ print(f"\nTo trim later, run:")
+ print(f" cs2pov trim \"{result.video_path}\" -d \"{demo_path}\" -p \"{args.player}\"")
+ else:
+ print(f"\nRecording ended with: {result.exit_reason}")
+ if result.video_path.exists():
+ size_mb = result.video_path.stat().st_size / (1024 * 1024)
+ print(f"Partial recording available: {result.video_path} ({size_mb:.1f} MB)")
return 0 if result.success else 1
@@ -958,52 +960,53 @@ def cmd_trim(args) -> int:
else:
output_path = video_path.parent / f"{video_path.stem}_trimmed{video_path.suffix}"
- # Parse demo and find player
- try:
- demo_info = parse_demo(demo_path)
- player = find_player(demo_info, args.player)
- player_slot = get_player_index(demo_info, player)
- except CS2POVError as e:
- print(f"Error: {e}", file=sys.stderr)
- return 1
-
- # Get timeline from demo
- timeline = None
- try:
- timeline = preprocess_demo(demo_path, player.steamid, player.name)
- print(f"Using demo timeline: {len(timeline.deaths)} deaths, {len(timeline.rounds)} rounds")
- print(f" {len(timeline.alive_segments)} alive segments to keep")
- except Exception as e:
- print(f"Warning: Could not preprocess demo: {e}")
- print("Falling back to console.log method")
-
- # Validate fallback parameters if needed
- if timeline is None:
- if args.console_log is None:
- print("Error: --console-log required when demo preprocessing fails", file=sys.stderr)
- return 1
- if args.player_slot is None:
- print("Error: --player-slot required when demo preprocessing fails", file=sys.stderr)
- return 1
- if args.recording_start_time is None:
- print("Error: --recording-start-time required when demo preprocessing fails", file=sys.stderr)
+ with LoadingAnimation():
+ # Parse demo and find player
+ try:
+ demo_info = parse_demo(demo_path)
+ player = find_player(demo_info, args.player)
+ player_slot = get_player_index(demo_info, player)
+ except CS2POVError as e:
+ print(f"Error: {e}", file=sys.stderr)
return 1
- player_slot = args.player_slot
- # Copy video to output path first (postprocess_video expects to rename)
- if video_path != output_path:
- shutil.copy2(video_path, output_path)
-
- # Run trimming
- postprocess_video(
- video_path=output_path,
- console_log_path=args.console_log.resolve() if args.console_log else Path("/dev/null"),
- player_slot=player_slot,
- recording_start_time=args.recording_start_time or 0.0,
- verbose=args.verbose,
- timeline=timeline,
- startup_time_override=args.startup_time,
- )
+ # Get timeline from demo
+ timeline = None
+ try:
+ timeline = preprocess_demo(demo_path, player.steamid, player.name)
+ print(f"Using demo timeline: {len(timeline.deaths)} deaths, {len(timeline.rounds)} rounds")
+ print(f" {len(timeline.alive_segments)} alive segments to keep")
+ except Exception as e:
+ print(f"Warning: Could not preprocess demo: {e}")
+ print("Falling back to console.log method")
+
+ # Validate fallback parameters if needed
+ if timeline is None:
+ if args.console_log is None:
+ print("Error: --console-log required when demo preprocessing fails", file=sys.stderr)
+ return 1
+ if args.player_slot is None:
+ print("Error: --player-slot required when demo preprocessing fails", file=sys.stderr)
+ return 1
+ if args.recording_start_time is None:
+ print("Error: --recording-start-time required when demo preprocessing fails", file=sys.stderr)
+ return 1
+ player_slot = args.player_slot
+
+ # Copy video to output path first (postprocess_video expects to rename)
+ if video_path != output_path:
+ shutil.copy2(video_path, output_path)
+
+ # Run trimming
+ postprocess_video(
+ video_path=output_path,
+ console_log_path=args.console_log.resolve() if args.console_log else Path("/dev/null"),
+ player_slot=player_slot,
+ recording_start_time=args.recording_start_time or 0.0,
+ verbose=args.verbose,
+ timeline=timeline,
+ startup_time_override=args.startup_time,
+ )
return 0
diff --git a/cs2pov/loading.py b/cs2pov/loading.py
index 6f3c1c6..5c57bb9 100644
--- a/cs2pov/loading.py
+++ b/cs2pov/loading.py
@@ -24,20 +24,25 @@ LOADING_MESSAGES = [
]
# =============================================================================
-# ANSI Color Codes
+# ANSI Escape Codes
# =============================================================================
-DIM = "\033[2m" # Dim/faint text
-RESET = "\033[0m" # Reset all attributes
+DIM = "\033[2m" # Dim/faint text
+RESET = "\033[0m" # Reset all attributes
CLEAR_LINE = "\033[2K\r" # Clear line and return to start
+CURSOR_UP = "\033[1A" # Move cursor up one line
class LoadingAnimation:
"""Animated loading message that oscillates periods.
+ The animation stays on the last line while regular output prints above it.
+
Usage:
with LoadingAnimation():
+ print("This prints above the animation")
do_slow_work()
+ print("So does this")
Or manually:
loader = LoadingAnimation()
@@ -59,48 +64,110 @@ class LoadingAnimation:
self.max_dots = max_dots
self._stop_event = threading.Event()
self._thread: threading.Thread | None = None
+ self._lock = threading.Lock()
+ self._active = False
+ self._current_dots = min_dots
+ self._stderr = None # Will hold reference to original stderr
+
+ def _draw(self):
+ """Draw the current animation state."""
+ dot_str = "." * self._current_dots
+ display = f"{DIM}{self.message}{dot_str}{RESET}"
+ self._stderr.write(f"{CLEAR_LINE}{display}")
+ self._stderr.flush()
def _animate(self):
"""Animation loop running in background thread."""
- dots = self.min_dots
direction = 1 # 1 = increasing, -1 = decreasing
while not self._stop_event.is_set():
- # Build the display string
- dot_str = "." * dots
- display = f"{DIM}{self.message}{dot_str}{RESET}"
-
- # Clear line and print (no newline)
- sys.stderr.write(f"{CLEAR_LINE}{display}")
- sys.stderr.flush()
+ with self._lock:
+ if self._active:
+ self._draw()
- # Update dot count
- dots += direction
- if dots >= self.max_dots:
- direction = -1
- elif dots <= self.min_dots:
- direction = 1
+ # Update dot count
+ self._current_dots += direction
+ if self._current_dots >= self.max_dots:
+ direction = -1
+ elif self._current_dots <= self.min_dots:
+ direction = 1
# Wait before next frame
self._stop_event.wait(0.3)
# Clear the line when done
- sys.stderr.write(CLEAR_LINE)
- sys.stderr.flush()
+ self._stderr.write(CLEAR_LINE)
+ self._stderr.flush()
+
+ def print(self, *args, **kwargs):
+ """Print while coordinating with the animation.
+
+ Use this instead of print() when you need output to appear above the animation.
+ """
+ with self._lock:
+ if self._active:
+ # Clear animation line, move up, then print will go there
+ self._stderr.write(CLEAR_LINE)
+ self._stderr.flush()
+
+ # Use the original print
+ print(*args, **kwargs)
+
+ if self._active:
+ # Redraw animation on the new line
+ self._draw()
def start(self):
"""Start the animation."""
self._stop_event.clear()
+ self._current_dots = self.min_dots
+ self._stderr = sys.stderr
+ self._active = True
+
+ # Install our print wrapper
+ import builtins
+ self._original_print = builtins.print
+ builtins.print = self._wrapped_print
+
self._thread = threading.Thread(target=self._animate, daemon=True)
self._thread.start()
+ def _wrapped_print(self, *args, **kwargs):
+ """Wrapped print function that coordinates with animation."""
+ with self._lock:
+ if self._active:
+ # Clear animation line
+ self._stderr.write(CLEAR_LINE)
+ self._stderr.flush()
+
+ # Call original print
+ self._original_print(*args, **kwargs)
+
+ # Flush to ensure output appears before animation redraws
+ file = kwargs.get('file', sys.stdout)
+ if hasattr(file, 'flush'):
+ file.flush()
+
+ if self._active:
+ # Redraw animation
+ self._draw()
+
def stop(self):
"""Stop the animation and clear the line."""
+ with self._lock:
+ self._active = False
+
self._stop_event.set()
+
if self._thread:
self._thread.join(timeout=1.0)
self._thread = None
+ # Restore original print
+ import builtins
+ if hasattr(self, '_original_print'):
+ builtins.print = self._original_print
+
def __enter__(self):
self.start()
return self