feat(capture): add toggle for mouse cursor display

This commit is contained in:
katR 2026-09-04 19:52:37 +05:30
parent 35f79a1cdd
commit 763cc04ad1
7 changed files with 121 additions and 11 deletions

View file

@ -76,6 +76,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn FrameSource> = 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<dyn std::error::Error + Send + Sync>> {
} 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<dyn std::error::Error + Send + Sync>> {
let (cmd_tx, cmd_rx) = bounded::<Command>(10);
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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() {

View file

@ -87,6 +87,26 @@ fn handle_toggle_recording() -> Result<(), Box<dyn std::error::Error + Send + Sy
}
}
fn handle_toggle_cursor() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
ensure_wayland_env();
@ -104,6 +124,9 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
"--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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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<dyn std::error::Error + Send + Sync>> {
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));

View file

@ -8,6 +8,10 @@ pub struct WaylandCapture {
impl WaylandCapture {
pub async fn new() -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
Self::new_with_cursor(true).await
}
pub async fn new_with_cursor(show_cursor: bool) -> Result<Self, Box<dyn std::error::Error + Send + Sync>> {
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);

View file

@ -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);
}
}

View file

@ -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 {

View file

@ -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 {

View file

@ -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<DaemonStatus>,
folder_tx: Sender<String>,
@ -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)