feat(capture): add toggle for mouse cursor display
This commit is contained in:
parent
35f79a1cdd
commit
763cc04ad1
7 changed files with 121 additions and 11 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue