diff options
Diffstat (limited to '')
| -rw-r--r-- | cs2pov/cli.py | 197 | ||||
| -rw-r--r-- | cs2pov/loading.py | 105 |
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 |
