""" KB Capture - Knowledge Base Recording Tool Main application that ties together: - Screen recording - Session/clip management - Hotkey control - System tray integration - GUI interface """ import sys import threading import time from pathlib import Path from typing import Optional, Callable from dataclasses import dataclass from enum import Enum from .recorder import ScreenRecorder, RecordingConfig from .session import SessionManager, Session, Clip, ClipStatus, SessionType from .hotkeys import HotkeyManager class AppState(Enum): """Application state machine.""" IDLE = "idle" # No session, ready to start SESSION_ACTIVE = "session" # Session started, not recording RECORDING = "recording" # Currently recording a clip PREVIEW = "preview" # Clip just recorded, awaiting decision @dataclass class AppStatus: """Current application status for UI updates.""" state: AppState session_name: Optional[str] = None project: Optional[str] = None session_type: Optional[SessionType] = None clip_count: int = 0 total_duration: float = 0.0 current_clip_duration: float = 0.0 last_clip_duration: float = 0.0 message: str = "" class KBCaptureApp: """ Main KB Capture application. Controls recording flow: 1. Start session (project, name, type) 2. Toggle recording to create clips 3. Keep or delete clips 4. End session to export """ def __init__( self, base_path: Path, on_status_change: Optional[Callable[[AppStatus], None]] = None, ): self.base_path = Path(base_path) self.on_status_change = on_status_change or (lambda x: None) # Components self.session_manager = SessionManager(self.base_path) self.recorder = ScreenRecorder(on_status=self._log) self.hotkeys = HotkeyManager() # State self.state = AppState.IDLE self._recording_start_time: Optional[float] = None self._duration_thread: Optional[threading.Thread] = None self._running = False # Setup hotkeys self.hotkeys.set_callback("toggle_recording", self.toggle_recording) self.hotkeys.set_callback("keep_clip", self.keep_last_clip) self.hotkeys.set_callback("delete_clip", self.delete_last_clip) self.hotkeys.set_callback("end_session", self.end_session) def _log(self, message: str) -> None: """Log a message and update status.""" print(f"[KB Capture] {message}") self._update_status(message=message) def _update_status(self, message: str = "") -> None: """Update status and notify listeners.""" session = self.session_manager.current_session status = AppStatus( state=self.state, session_name=session.name if session else None, project=session.project if session else None, session_type=session.session_type if session else None, clip_count=session.clip_count if session else 0, total_duration=session.total_duration if session else 0.0, current_clip_duration=self.recorder.get_duration() if self.state == AppState.RECORDING else 0.0, last_clip_duration=self._get_last_clip_duration(), message=message, ) self.on_status_change(status) def _get_last_clip_duration(self) -> float: """Get duration of the last clip.""" session = self.session_manager.current_session if session and session.clips: return session.clips[-1].duration_seconds return 0.0 def _start_duration_thread(self) -> None: """Start thread to update duration while recording.""" self._running = True def update_loop(): while self._running and self.state == AppState.RECORDING: self._update_status() time.sleep(0.5) self._duration_thread = threading.Thread(target=update_loop, daemon=True) self._duration_thread.start() def _stop_duration_thread(self) -> None: """Stop duration update thread.""" self._running = False if self._duration_thread: self._duration_thread.join(timeout=1) self._duration_thread = None # === Public API === def start(self) -> None: """Start the application (register hotkeys).""" self.hotkeys.register_all() self._log("KB Capture started. Hotkeys active.") def stop(self) -> None: """Stop the application.""" self.hotkeys.unregister_all() self._stop_duration_thread() if self.state == AppState.RECORDING: self.recorder.stop() self._log("KB Capture stopped.") def start_session( self, name: str, project: str, session_type: SessionType = SessionType.DESIGN, ) -> Session: """Start a new recording session.""" if self.state != AppState.IDLE: raise RuntimeError("Session already active") session = self.session_manager.start_session(name, project, session_type) self.state = AppState.SESSION_ACTIVE self._log(f"Session started: {name}") self._update_status() return session def toggle_recording(self) -> None: """Toggle recording state (hotkey handler).""" if self.state == AppState.IDLE: self._log("No session active. Start a session first.") return if self.state == AppState.RECORDING: self._stop_recording() else: self._start_recording() def _start_recording(self) -> None: """Start recording a new clip.""" if self.state not in (AppState.SESSION_ACTIVE, AppState.PREVIEW): return # Auto-keep any clip in preview if self.state == AppState.PREVIEW: self.session_manager.keep_last_clip() # Start new clip clip, clip_path = self.session_manager.start_clip() config = RecordingConfig( output_path=clip_path, framerate=30, ) if self.recorder.start(config): self.state = AppState.RECORDING self._start_duration_thread() self._log(f"Recording clip {clip.id}...") else: self._log("Failed to start recording") def _stop_recording(self) -> None: """Stop recording current clip.""" if self.state != AppState.RECORDING: return self._stop_duration_thread() duration = self.recorder.get_duration() output = self.recorder.stop() if output and output.exists(): self.session_manager.end_clip(duration) self.state = AppState.PREVIEW self._log(f"Clip recorded: {duration:.1f}s - Keep (K) or Delete (D)?") else: self._log("Recording failed - no output") self.state = AppState.SESSION_ACTIVE self._update_status() def keep_last_clip(self, note: str = "") -> Optional[Clip]: """Keep the last recorded clip.""" clip = self.session_manager.keep_last_clip(note) if clip: self.state = AppState.SESSION_ACTIVE self._log(f"Kept clip: {clip.id}") self._update_status() return clip def delete_last_clip(self) -> Optional[Clip]: """Delete the last recorded clip.""" clip = self.session_manager.delete_last_clip() if clip: self.state = AppState.SESSION_ACTIVE self._log(f"Deleted clip: {clip.id}") self._update_status() return clip def end_session(self) -> Optional[Session]: """End current session and prepare for export.""" if self.state == AppState.IDLE: return None # Stop recording if active if self.state == AppState.RECORDING: self._stop_recording() # Keep any preview clips if self.state == AppState.PREVIEW: self.session_manager.keep_last_clip() session = self.session_manager.end_session() self.state = AppState.IDLE self._log(f"Session ended: {session.clip_count} clips, {session.total_duration:.1f}s total") self._update_status() return session def cancel_session(self) -> None: """Cancel current session and delete all clips.""" if self.state == AppState.RECORDING: self._stop_duration_thread() self.recorder.stop() self.session_manager.cancel_session() self.state = AppState.IDLE self._log("Session cancelled") self._update_status() def get_status(self) -> AppStatus: """Get current application status.""" session = self.session_manager.current_session return AppStatus( state=self.state, session_name=session.name if session else None, project=session.project if session else None, session_type=session.session_type if session else None, clip_count=session.clip_count if session else 0, total_duration=session.total_duration if session else 0.0, current_clip_duration=self.recorder.get_duration() if self.state == AppState.RECORDING else 0.0, last_clip_duration=self._get_last_clip_duration(), ) def list_sessions(self) -> list[Session]: """List all recorded sessions.""" return self.session_manager.list_sessions() def get_session_dir(self, session_id: str) -> Path: """Get session directory for export.""" return self.session_manager.get_session_dir(session_id) # CLI for testing if __name__ == "__main__": import tempfile def on_status(status: AppStatus): state_icons = { AppState.IDLE: "⚪", AppState.SESSION_ACTIVE: "🟢", AppState.RECORDING: "🔴", AppState.PREVIEW: "🟡", } icon = state_icons.get(status.state, "⚪") print(f"\n{icon} State: {status.state.value}") if status.session_name: print(f" Session: {status.session_name} ({status.project})") print(f" Clips: {status.clip_count} | Duration: {status.total_duration:.1f}s") if status.current_clip_duration > 0: print(f" Recording: {status.current_clip_duration:.1f}s") if status.message: print(f" → {status.message}") with tempfile.TemporaryDirectory() as tmpdir: app = KBCaptureApp( base_path=Path(tmpdir), on_status_change=on_status, ) print("\n=== KB Capture Test ===") print("Commands:") print(" s - Start session") print(" r - Toggle recording") print(" k - Keep last clip") print(" d - Delete last clip") print(" e - End session") print(" c - Cancel session") print(" q - Quit") print() app.start() try: while True: cmd = input("> ").strip().lower() if cmd == "s": name = input("Session name: ").strip() or "Test Session" project = input("Project: ").strip() or "P04-GigaBIT-M1" app.start_session(name, project) elif cmd == "r": app.toggle_recording() elif cmd == "k": app.keep_last_clip() elif cmd == "d": app.delete_last_clip() elif cmd == "e": app.end_session() elif cmd == "c": app.cancel_session() elif cmd == "q": break else: print("Unknown command") except KeyboardInterrupt: pass app.stop() print("\nGoodbye!")