diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index 31546c2..f364e9f 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -76,6 +76,8 @@ async fn main() -> Result<(), Box> { let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); println!("Detected session type: {}", session_type); + let initial_config = vrec::config::VrecConfig::load(); + let mut source: Box = if std::env::args().any(|a| a == "--mock") { println!("Initializing MOCK capture..."); Box::new(vrec::capture::mock::MockCapture::new()) @@ -92,8 +94,8 @@ async fn main() -> Result<(), Box> { } else if session_type.to_lowercase() == "wayland" { #[cfg(target_os = "linux")] { - println!("Initializing Wayland capture..."); - Box::new(vrec::capture::wayland::WaylandCapture::new().await?) + println!("Initializing Wayland capture (cursor: {})...", initial_config.show_cursor); + Box::new(vrec::capture::wayland::WaylandCapture::new_with_cursor(initial_config.show_cursor).await?) } #[cfg(not(target_os = "linux"))] { @@ -123,7 +125,6 @@ async fn main() -> Result<(), Box> { let (cmd_tx, cmd_rx) = bounded::(10); let (mux_tx, mux_rx) = bounded::>(1); let (audio_tx, audio_rx) = bounded::>(500); - let initial_config = vrec::config::VrecConfig::load(); let audio_capture_initial = vrec::capture::audio::AudioCapture::new_with_device_and_mode( audio_tx.clone(), Some(&initial_config.audio_device), @@ -281,7 +282,16 @@ async fn main() -> Result<(), Box> { if let Ok(cmd) = cmd_res { match cmd { Command::ReloadConfig => { - config = vrec::config::VrecConfig::load(); + let new_config = vrec::config::VrecConfig::load(); + if new_config.show_cursor != config.show_cursor { + println!("Cursor display changed ({} -> {}). Restarting daemon for new capture session...", config.show_cursor, new_config.show_cursor); + if let Some(mut m) = normal_muxer.take() { + let _ = m.finalize(); + } + std::thread::sleep(std::time::Duration::from_millis(150)); + std::process::exit(0); + } + config = new_config; replay_state_clone.store(config.replay_enabled, Ordering::SeqCst); audio_muted_clone.store(config.audio_mode == "muted", Ordering::SeqCst); println!("Daemon config reloaded! Audio mode: {}", config.audio_mode); @@ -501,6 +511,7 @@ async fn main() -> Result<(), Box> { is_replay_active: replay_enabled_state.load(Ordering::SeqCst), audio_muted: audio_muted_state.load(Ordering::SeqCst), audio_mode: cfg.audio_mode, + show_cursor: cfg.show_cursor, }; if let Ok(resp) = serde_json::to_vec(&status) { let len_resp = (resp.len() as u32).to_le_bytes(); @@ -508,6 +519,15 @@ async fn main() -> Result<(), Box> { let _ = stream.write_all(&resp); } }, + Command::ToggleCursor => { + let mut cfg = vrec::config::VrecConfig::load(); + cfg.show_cursor = !cfg.show_cursor; + let _ = cfg.save(); + println!("Cursor display toggled to: {}. Restarting daemon for new capture session...", cfg.show_cursor); + let _ = cmd_tx.send(Command::StopRecording); + std::thread::sleep(std::time::Duration::from_millis(150)); + std::process::exit(0); + }, Command::CycleAudioMode => { let mut cfg = vrec::config::VrecConfig::load(); cfg.audio_mode = match cfg.audio_mode.as_str() { diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index db25029..38d5b3a 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -87,6 +87,26 @@ fn handle_toggle_recording() -> Result<(), Box Result<(), Box> { + ensure_daemon_running(); + let current_cursor = query_status().ok().map(|s| s.show_cursor).unwrap_or(true); + let next_cursor = !current_cursor; + match send_command(Command::ToggleCursor) { + Ok(()) => { + if next_cursor { + show_notification("Cursor: Visible in recording"); + } else { + show_notification("Cursor: Hidden from recording"); + } + Ok(()) + } + Err(e) => { + show_notification("Error: failed to connect to vrec-daemon"); + Err(e) + } + } +} + fn main() -> Result<(), Box> { ensure_wayland_env(); @@ -104,6 +124,9 @@ fn main() -> Result<(), Box> { "--record" | "--toggle" => { return handle_toggle_recording(); } + "--cursor" | "--toggle-cursor" => { + return handle_toggle_cursor(); + } "--start" => { return send_with_notification(Command::StartRecording, "Recording started"); } @@ -123,6 +146,7 @@ fn main() -> Result<(), Box> { println!(" vrec-ui --menu Open overlay menu"); println!(" vrec-ui --save Save instant replay and show notification"); println!(" vrec-ui --record Toggle normal recording on/off (with notification)"); + println!(" vrec-ui --cursor Toggle mouse cursor recording on/off (with notification)"); println!(" vrec-ui --start Start normal recording"); println!(" vrec-ui --stop Stop normal recording"); println!(" vrec-ui --reload Reload daemon configuration"); @@ -159,12 +183,14 @@ fn main() -> Result<(), Box> { let hotkey_menu = HotKey::new(Some(Modifiers::ALT), Code::KeyZ); let hotkey_save = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR); let hotkey_record = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9); + let hotkey_cursor = HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F10); let _ = manager.register(hotkey_menu); let _ = manager.register(hotkey_save); let _ = manager.register(hotkey_record); + let _ = manager.register(hotkey_cursor); - println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording)..."); + println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording, Ctrl+Shift+F10 for cursor)..."); let receiver = GlobalHotKeyEvent::receiver(); loop { @@ -176,6 +202,8 @@ fn main() -> Result<(), Box> { let _ = send_with_notification(Command::SaveReplay, "Replay saved"); } else if event.id == hotkey_record.id() { let _ = handle_toggle_recording(); + } else if event.id == hotkey_cursor.id() { + let _ = handle_toggle_cursor(); } } std::thread::sleep(std::time::Duration::from_millis(50)); diff --git a/src/capture/wayland.rs b/src/capture/wayland.rs index 1e4300e..e6963b5 100644 --- a/src/capture/wayland.rs +++ b/src/capture/wayland.rs @@ -8,6 +8,10 @@ pub struct WaylandCapture { impl WaylandCapture { pub async fn new() -> Result> { + Self::new_with_cursor(true).await + } + + pub async fn new_with_cursor(show_cursor: bool) -> Result> { let proxy = Screencast::new().await.map_err(|e| { format!( "Failed to initialize XDG ScreenCast portal ({:?}).\n\ @@ -23,10 +27,16 @@ impl WaylandCapture { let session = proxy.create_session(Default::default()).await.map_err(|e| { format!("Failed to create ScreenCast portal session: {:?}", e) })?; + + let cursor_mode = if show_cursor { + CursorMode::Embedded + } else { + CursorMode::Hidden + }; let select_opts = ashpd::desktop::screencast::SelectSourcesOptions::default() .set_multiple(false) - .set_cursor_mode(CursorMode::Embedded) + .set_cursor_mode(cursor_mode) .set_sources(SourceType::Monitor | SourceType::Monitor) .set_persist_mode(PersistMode::DoNot); diff --git a/src/config.rs b/src/config.rs index f6a6b02..000b93c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -15,6 +15,7 @@ fn default_theme() -> String { "dark".to_string() } fn default_save_hotkey() -> String { "Ctrl+Shift+R".to_string() } fn default_menu_hotkey() -> String { "Alt+Z".to_string() } fn default_record_hotkey() -> String { "Ctrl+Shift+F9".to_string() } +fn default_cursor_hotkey() -> String { "Ctrl+Shift+F10".to_string() } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VrecConfig { @@ -35,6 +36,8 @@ pub struct VrecConfig { pub video_codec: String, #[serde(default = "VrecConfig::default_output_directory")] pub output_directory: String, + #[serde(default = "default_true")] + pub show_cursor: bool, #[serde(default = "default_audio_device")] pub audio_device: String, #[serde(default = "default_audio_mode")] @@ -52,6 +55,8 @@ pub struct VrecConfig { pub menu_hotkey: String, #[serde(default = "default_record_hotkey")] pub record_hotkey: String, + #[serde(default = "default_cursor_hotkey")] + pub cursor_hotkey: String, } impl Default for VrecConfig { @@ -66,6 +71,7 @@ impl Default for VrecConfig { fps: 60, video_codec: "h264".to_string(), output_directory: Self::default_output_directory(), + show_cursor: true, audio_device: "default".to_string(), audio_mode: "system".to_string(), @@ -75,6 +81,7 @@ impl Default for VrecConfig { save_hotkey: "Ctrl+Shift+R".to_string(), menu_hotkey: "Alt+Z".to_string(), record_hotkey: "Ctrl+Shift+F9".to_string(), + cursor_hotkey: "Ctrl+Shift+F10".to_string(), } } } @@ -138,9 +145,11 @@ mod tests { assert_eq!(cfg.fps, 60); assert_eq!(cfg.video_codec, "h264"); assert_eq!(cfg.audio_mode, "system"); + assert!(cfg.show_cursor); let json = serde_json::to_string(&cfg).unwrap(); let parsed: VrecConfig = serde_json::from_str(&json).unwrap(); assert_eq!(parsed.replay_duration_sec, 60); assert_eq!(parsed.audio_mode, "system"); + assert!(parsed.show_cursor); } } diff --git a/src/hyprland_binds.rs b/src/hyprland_binds.rs index 307ec45..fb1af4b 100644 --- a/src/hyprland_binds.rs +++ b/src/hyprland_binds.rs @@ -87,6 +87,7 @@ pub fn register_hyprland_binds(config: &VrecConfig) { (&config.menu_hotkey, "exec, vrec-ui --menu"), (&config.save_hotkey, "exec, vrec-ui --save"), (&config.record_hotkey, "exec, vrec-ui --record"), + (&config.cursor_hotkey, "exec, vrec-ui --cursor"), ]; for (hotkey, action) in binds { @@ -124,6 +125,7 @@ pub fn unregister_hyprland_binds(config: &VrecConfig) { &config.menu_hotkey, &config.save_hotkey, &config.record_hotkey, + &config.cursor_hotkey, ]; for hotkey in hotkeys { diff --git a/src/ipc.rs b/src/ipc.rs index d72b406..a8a528f 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -8,6 +8,7 @@ pub enum Command { ToggleRecording, ToggleAudio, CycleAudioMode, + ToggleCursor, ReloadConfig, StopDaemon, ShowOverlay, @@ -22,6 +23,12 @@ pub struct DaemonStatus { pub audio_muted: bool, #[serde(default = "default_audio_mode_str")] pub audio_mode: String, + #[serde(default = "default_true")] + pub show_cursor: bool, +} + +fn default_true() -> bool { + true } fn default_audio_mode_str() -> String { diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index df05361..ffd3a05 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -21,6 +21,7 @@ pub struct VrecOverlayApp { bitrate_mbps: u32, target_fps: u32, audio_mode_idx: usize, + show_cursor: bool, status_msg: Option<(String, Instant)>, status_rx: Receiver, folder_tx: Sender, @@ -34,6 +35,7 @@ impl VrecOverlayApp { let bitrate_mbps = (config.record_bitrate_kbps / 1000).max(1); let target_fps = config.fps; let output_dir = config.output_directory.clone(); + let show_cursor = config.show_cursor; let audio_mode_idx = match config.audio_mode.as_str() { "mic" => 1, "both" => 2, @@ -65,6 +67,7 @@ impl VrecOverlayApp { bitrate_mbps, target_fps, audio_mode_idx, + show_cursor, status_msg: None, status_rx, folder_tx, @@ -75,6 +78,9 @@ impl VrecOverlayApp { fn poll_async_events(&mut self) { // Drain any status updates from background thread while let Ok(s) = self.status_rx.try_recv() { + if self.current_tab == OverlayTab::Dashboard { + self.show_cursor = s.show_cursor; + } self.status = s; self.daemon_connected = true; } @@ -215,6 +221,24 @@ impl eframe::App for VrecOverlayApp { }); } + // Quick Cursor Toggle Button + let (cursor_label, cursor_color, cursor_bg) = if self.show_cursor { + ("Cursor: ON", Color32::from_rgb(52, 211, 153), Color32::from_rgb(20, 35, 30)) + } else { + ("Cursor: OFF", Color32::from_rgb(148, 163, 184), Color32::from_rgb(24, 30, 42)) + }; + let cursor_btn = egui::Button::new(egui::RichText::new(cursor_label).size(11.0).color(cursor_color)) + .fill(cursor_bg) + .stroke(Stroke::new(1.0_f32, Color32::from_rgb(45, 55, 75))) + .corner_radius(CornerRadius::same(6_u8)); + if ui.add_sized([84.0, 26.0], cursor_btn).clicked() { + self.show_cursor = !self.show_cursor; + self.config.show_cursor = self.show_cursor; + let _ = ipc::send_command(Command::ToggleCursor); + let msg = if self.show_cursor { "Cursor set to ON" } else { "Cursor set to OFF" }; + self.set_msg(msg); + } + // Tab Switcher Pills let settings_active = self.current_tab == OverlayTab::Settings; let settings_fill = if settings_active { Color32::from_rgb(37, 99, 235) } else { Color32::from_rgb(24, 29, 41) }; @@ -225,7 +249,7 @@ impl eframe::App for VrecOverlayApp { .corner_radius(CornerRadius::same(6_u8)); if ui.add_sized([78.0, 26.0], settings_btn).clicked() { self.current_tab = OverlayTab::Settings; - ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(780.0, 480.0))); + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(820.0, 490.0))); } let dash_active = self.current_tab == OverlayTab::Dashboard; @@ -237,7 +261,7 @@ impl eframe::App for VrecOverlayApp { .corner_radius(CornerRadius::same(6_u8)); if ui.add_sized([84.0, 26.0], dash_btn).clicked() { self.current_tab = OverlayTab::Dashboard; - ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(780.0, 280.0))); + ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(Vec2::new(820.0, 280.0))); } }); }); @@ -523,6 +547,15 @@ impl eframe::App for VrecOverlayApp { if pill(ui, "50M", self.bitrate_mbps == 50) { self.bitrate_mbps = 50; } }); + ui.add_space(10.0); + + // Mouse Cursor Capture + ui.label(egui::RichText::new("Mouse Cursor Capture").font(FontId::proportional(11.0)).color(Color32::from_rgb(203, 213, 225))); + ui.horizontal(|ui| { + if pill(ui, "Show Cursor", self.show_cursor) { self.show_cursor = true; } + if pill(ui, "Hide Cursor", !self.show_cursor) { self.show_cursor = false; } + }); + ui.add_space(8.0); ui.label(egui::RichText::new("Codec: Hardware VAAPI H.264 (NV12 Direct)").font(FontId::monospace(9.0)).color(Color32::from_rgb(100, 116, 139))); }); @@ -589,6 +622,7 @@ impl eframe::App for VrecOverlayApp { self.config.record_bitrate_kbps = self.bitrate_mbps * 1000; self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000; self.config.fps = self.target_fps; + self.config.show_cursor = self.show_cursor; self.config.audio_mode = match self.audio_mode_idx { 1 => "mic", 2 => "both", @@ -619,9 +653,9 @@ pub fn run_egui_overlay() { viewport: egui::ViewportBuilder::default() .with_title("vrec") .with_app_id("vrec-overlay") - .with_inner_size([780.0, 280.0]) - .with_min_inner_size([680.0, 240.0]) - .with_max_inner_size([880.0, 560.0]) + .with_inner_size([820.0, 280.0]) + .with_min_inner_size([720.0, 240.0]) + .with_max_inner_size([920.0, 560.0]) .with_resizable(true) .with_decorations(false) .with_transparent(true)