diff --git a/src/bin/vrec-daemon.rs b/src/bin/vrec-daemon.rs index b70ec10..aa185ca 100644 --- a/src/bin/vrec-daemon.rs +++ b/src/bin/vrec-daemon.rs @@ -12,8 +12,42 @@ use std::io::{Read, Write}; use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; use std::sync::Arc; +fn ensure_wayland_env() { + #[cfg(target_os = "linux")] + { + let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() })); + unsafe { + if env::var("WAYLAND_DISPLAY").is_err() { + if let Ok(entries) = std::fs::read_dir(&runtime_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("wayland-") && !name.ends_with(".lock") { + env::set_var("WAYLAND_DISPLAY", &name); + println!("Auto-detected Wayland display: {}", name); + break; + } + } + } + } + if env::var("DBUS_SESSION_BUS_ADDRESS").is_err() { + let bus_path = format!("{}/bus", runtime_dir); + if std::path::Path::new(&bus_path).exists() { + env::set_var("DBUS_SESSION_BUS_ADDRESS", format!("unix:path={}", bus_path)); + } + } + if env::var("XDG_CURRENT_DESKTOP").is_err() { + env::set_var("XDG_CURRENT_DESKTOP", "Hyprland"); + } + if env::var("XDG_SESSION_TYPE").map(|s| s == "tty" || s.is_empty()).unwrap_or(true) && env::var("WAYLAND_DISPLAY").is_ok() { + env::set_var("XDG_SESSION_TYPE", "wayland"); + } + } + } +} + #[tokio::main] async fn main() -> Result<(), Box> { + ensure_wayland_env(); let session_type = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "x11".to_string()); println!("Detected session type: {}", session_type); @@ -65,7 +99,11 @@ async fn main() -> Result<(), Box> { let (mux_tx, mux_rx) = bounded::>(1); let (audio_tx, audio_rx) = bounded::>(100); let initial_config = vrec::config::VrecConfig::load(); - let audio_info = vrec::capture::audio::AudioCapture::new_with_device(audio_tx, Some(&initial_config.audio_device)).ok(); + let audio_info = vrec::capture::audio::AudioCapture::new_with_device_and_mode( + audio_tx, + Some(&initial_config.audio_device), + &initial_config.audio_mode, + ).ok(); let is_recording_state = Arc::new(AtomicBool::new(false)); let record_start_state = Arc::new(AtomicU64::new(0)); @@ -306,11 +344,13 @@ async fn main() -> Result<(), Box> { } else { 0 }; + let cfg = vrec::config::VrecConfig::load(); let status = DaemonStatus { is_recording: rec, recording_duration_sec: duration, is_replay_active: replay_enabled_state.load(Ordering::SeqCst), audio_muted: audio_muted_state.load(Ordering::SeqCst), + audio_mode: cfg.audio_mode, }; if let Ok(resp) = serde_json::to_vec(&status) { let len_resp = (resp.len() as u32).to_le_bytes(); @@ -318,6 +358,17 @@ async fn main() -> Result<(), Box> { let _ = stream.write_all(&resp); } }, + Command::CycleAudioMode => { + let mut cfg = vrec::config::VrecConfig::load(); + cfg.audio_mode = match cfg.audio_mode.as_str() { + "system" => "mic", + "mic" => "both", + "both" => "muted", + _ => "system", + }.to_string(); + let _ = cfg.save(); + println!("Audio mode cycled to: {}", cfg.audio_mode); + }, Command::StopDaemon => { println!("StopDaemon requested: Finalizing active recordings..."); let _ = cmd_tx.send(Command::StopRecording); diff --git a/src/bin/vrec-ui.rs b/src/bin/vrec-ui.rs index 5615bfb..c834b18 100644 --- a/src/bin/vrec-ui.rs +++ b/src/bin/vrec-ui.rs @@ -3,20 +3,72 @@ use vrec::overlay::{show_menu_overlay, show_notification}; use std::env; use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent}; +fn ensure_wayland_env() { + #[cfg(target_os = "linux")] + { + let runtime_dir = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| format!("/run/user/{}", unsafe { libc::getuid() })); + unsafe { + if env::var("WAYLAND_DISPLAY").is_err() { + if let Ok(entries) = std::fs::read_dir(&runtime_dir) { + for entry in entries.flatten() { + let name = entry.file_name().to_string_lossy().to_string(); + if name.starts_with("wayland-") && !name.ends_with(".lock") { + env::set_var("WAYLAND_DISPLAY", &name); + break; + } + } + } + } + if env::var("DBUS_SESSION_BUS_ADDRESS").is_err() { + let bus_path = format!("{}/bus", runtime_dir); + if std::path::Path::new(&bus_path).exists() { + env::set_var("DBUS_SESSION_BUS_ADDRESS", format!("unix:path={}", bus_path)); + } + } + if env::var("XDG_CURRENT_DESKTOP").is_err() { + env::set_var("XDG_CURRENT_DESKTOP", "Hyprland"); + } + if env::var("XDG_SESSION_TYPE").map(|s| s == "tty" || s.is_empty()).unwrap_or(true) && env::var("WAYLAND_DISPLAY").is_ok() { + env::set_var("XDG_SESSION_TYPE", "wayland"); + } + } + } +} + +fn ensure_daemon_running() { + if query_status().is_err() { + println!("vrec-daemon not running. Auto-launching vrec-daemon..."); + let _ = std::process::Command::new("vrec-daemon") + .stdin(std::process::Stdio::null()) + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .spawn(); + for _ in 0..15 { + std::thread::sleep(std::time::Duration::from_millis(100)); + if query_status().is_ok() { + println!("vrec-daemon successfully connected."); + break; + } + } + } +} + fn send_with_notification(cmd: Command, success_msg: &str) -> Result<(), Box> { + ensure_daemon_running(); match send_command(cmd) { Ok(()) => { show_notification(success_msg); Ok(()) } Err(e) => { - show_notification("Error: vrec-daemon is not running"); + show_notification("Error: failed to connect to vrec-daemon"); Err(e) } } } fn handle_toggle_recording() -> Result<(), Box> { + ensure_daemon_running(); let status = query_status().ok(); let is_rec = status.as_ref().map(|s| s.is_recording).unwrap_or(false); match send_command(Command::ToggleRecording) { @@ -29,13 +81,15 @@ fn handle_toggle_recording() -> Result<(), Box { - show_notification("Error: vrec-daemon is not running"); + show_notification("Error: failed to connect to vrec-daemon"); Err(e) } } } fn main() -> Result<(), Box> { + ensure_wayland_env(); + let args: Vec = env::args().collect(); if args.len() > 1 { match args[1].as_str() { @@ -43,6 +97,7 @@ fn main() -> Result<(), Box> { return send_with_notification(Command::SaveReplay, "Replay saved"); } "--menu" => { + ensure_daemon_running(); show_menu_overlay(); return Ok(()); } @@ -98,55 +153,29 @@ fn main() -> Result<(), Box> { println!("X11 session detected."); } - let manager = GlobalHotKeyManager::new().map_err(|e| format!("Failed to init GlobalHotKeyManager: {:?}", e))?; - let config = vrec::config::VrecConfig::load(); - let save_hotkey = vrec::hotkey::parse_hotkey(&config.save_hotkey) - .unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::KeyR)); - let menu_hotkey = vrec::hotkey::parse_hotkey(&config.menu_hotkey) - .unwrap_or_else(|| HotKey::new(Some(Modifiers::ALT), Code::KeyZ)); - let record_hotkey = vrec::hotkey::parse_hotkey(&config.record_hotkey) - .unwrap_or_else(|| HotKey::new(Some(Modifiers::CONTROL | Modifiers::SHIFT), Code::F9)); - - let mut registered_count = 0; - if let Err(e) = manager.register(save_hotkey) { - eprintln!("Warning: Failed to register save hotkey ({}): {}", config.save_hotkey, e); - } else { - println!("Registered save replay hotkey: {}", config.save_hotkey); - registered_count += 1; - } + let manager = GlobalHotKeyManager::new()?; + 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); - if let Err(e) = manager.register(menu_hotkey) { - eprintln!("Warning: Failed to register menu hotkey ({}): {}", config.menu_hotkey, e); - } else { - println!("Registered menu overlay hotkey: {}", config.menu_hotkey); - registered_count += 1; - } + let _ = manager.register(hotkey_menu); + let _ = manager.register(hotkey_save); + let _ = manager.register(hotkey_record); - if let Err(e) = manager.register(record_hotkey) { - eprintln!("Warning: Failed to register record hotkey ({}): {}", config.record_hotkey, e); - } else { - println!("Registered toggle recording hotkey: {}", config.record_hotkey); - registered_count += 1; - } - - if registered_count == 0 { - eprintln!("Note: Running without global hotkeys. You can bind `vrec-ui --menu`, `vrec-ui --record`, and `vrec-ui --save` to compositor shortcuts."); - } + println!("Listening for global hotkeys (Alt+Z for overlay, Ctrl+Shift+R for replay, Ctrl+Shift+F9 for recording)..."); let receiver = GlobalHotKeyEvent::receiver(); loop { - if let Ok(event) = receiver.recv() - && event.state == global_hotkey::HotKeyState::Pressed { - if event.id == save_hotkey.id() { - println!("Save replay hotkey pressed. Triggering SaveReplay..."); - let _ = send_with_notification(Command::SaveReplay, "Replay saved"); - } else if event.id == menu_hotkey.id() { - println!("Menu hotkey pressed. Opening Overlay Menu..."); - show_menu_overlay(); - } else if event.id == record_hotkey.id() { - println!("Record hotkey pressed. Toggling Recording..."); - let _ = handle_toggle_recording(); - } + if let Ok(event) = receiver.try_recv() { + if event.id == hotkey_menu.id() { + ensure_daemon_running(); + show_menu_overlay(); + } else if event.id == hotkey_save.id() { + let _ = send_with_notification(Command::SaveReplay, "Replay saved"); + } else if event.id == hotkey_record.id() { + let _ = handle_toggle_recording(); } + } + std::thread::sleep(std::time::Duration::from_millis(50)); } } diff --git a/src/capture/audio.rs b/src/capture/audio.rs index 2e09c42..e921595 100644 --- a/src/capture/audio.rs +++ b/src/capture/audio.rs @@ -1,8 +1,10 @@ use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; use crossbeam_channel::Sender; +use std::collections::VecDeque; +use std::thread; pub struct AudioCapture { - _stream: cpal::Stream, + _streams: Vec, } pub fn get_device_name(dev: &cpal::Device) -> String { @@ -27,75 +29,168 @@ pub fn list_input_devices() -> Vec { impl AudioCapture { pub fn new(sender: Sender>) -> Result<(Self, u32, u16), Box> { - Self::new_with_device(sender, None) + Self::new_with_device_and_mode(sender, None, "system") } pub fn new_with_device( sender: Sender>, device_name: Option<&str>, + ) -> Result<(Self, u32, u16), Box> { + Self::new_with_device_and_mode(sender, device_name, "system") + } + + pub fn new_with_device_and_mode( + sender: Sender>, + device_name: Option<&str>, + audio_mode: &str, ) -> Result<(Self, u32, u16), Box> { let host = cpal::default_host(); - let device = if let Some(target) = device_name - && target != "default" - && !target.trim().is_empty() { - if let Ok(mut devs) = host.input_devices() { - devs.find(|d| get_device_name(d) == target) - .or_else(|| host.default_input_device()) - .ok_or("Audio device not found")? - } else { - host.default_input_device().ok_or("No audio input device available")? + + if audio_mode == "muted" { + println!("Audio capture mode: MUTED (No audio recorded)"); + return Ok((Self { _streams: Vec::new() }, 48000, 2)); + } + + // Helper to find monitor device (system sound) + let find_system_device = || -> Option { + if let Ok(devs) = host.input_devices() { + for d in devs { + let n = get_device_name(&d).to_lowercase(); + if n.contains("monitor") { + return Some(d); + } } - } else { - // When default is selected, prefer desktop audio monitor if available so gameplay sound is recorded - if let Ok(mut devs) = host.input_devices() { - devs.find(|d| { - let n = get_device_name(d).to_lowercase(); - n.contains("monitor") - }) - .or_else(|| host.default_input_device()) - .ok_or("No audio input device available")? - } else { - host.default_input_device().ok_or("No audio input device available")? - } - }; - - let dev_name = get_device_name(&device); - let config = device.default_input_config()?; - let sample_rate = config.sample_rate(); - let channels = config.channels(); - - println!("Audio capture initialized on [{}]: {} Hz, {} channels", dev_name, sample_rate, channels); - - let err_fn = |err| eprintln!("Audio stream error: {}", err); - let stream_config: cpal::StreamConfig = config.into(); - - let stream = match config.sample_format() { - cpal::SampleFormat::F32 => { - device.build_input_stream( - stream_config, - move |data: &[f32], _: &_| { - let _ = sender.try_send(data.to_vec()); - }, - err_fn, - None, - )? } - cpal::SampleFormat::I16 => { - device.build_input_stream( - stream_config, - move |data: &[i16], _: &_| { - let f32_data: Vec = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect(); - let _ = sender.try_send(f32_data); - }, - err_fn, - None, - )? - } - _ => return Err("Unsupported audio format".into()), + host.default_input_device() }; - - stream.play()?; - - Ok((Self { _stream: stream }, sample_rate, channels)) + + // Helper to find microphone device + let find_mic_device = || -> Option { + if let Some(target) = device_name + && target != "default" + && !target.trim().is_empty() { + if let Ok(devs) = host.input_devices() { + for d in devs { + if get_device_name(&d) == target { + return Some(d); + } + } + } + } + if let Ok(devs) = host.input_devices() { + for d in devs { + let n = get_device_name(&d).to_lowercase(); + if !n.contains("monitor") { + return Some(d); + } + } + } + host.default_input_device() + }; + + // Helper to build a stream for a device + let build_stream = |dev: &cpal::Device, tx: Sender>| -> Result<(cpal::Stream, u32, u16), Box> { + let config = dev.default_input_config()?; + let sample_rate = config.sample_rate(); + let channels = config.channels(); + let format = config.sample_format(); + let err_fn = |err| eprintln!("Audio stream error: {}", err); + let stream_config: cpal::StreamConfig = config.into(); + + let stream = match format { + cpal::SampleFormat::F32 => { + dev.build_input_stream( + stream_config, + move |data: &[f32], _: &_| { + let _ = tx.try_send(data.to_vec()); + }, + err_fn, + None, + )? + } + cpal::SampleFormat::I16 => { + dev.build_input_stream( + stream_config, + move |data: &[i16], _: &_| { + let f32_data: Vec = data.iter().map(|&s| s as f32 / i16::MAX as f32).collect(); + let _ = tx.try_send(f32_data); + }, + err_fn, + None, + )? + } + _ => return Err("Unsupported audio format".into()), + }; + stream.play()?; + Ok((stream, sample_rate, channels)) + }; + + match audio_mode { + "mic" => { + let dev = find_mic_device().ok_or("No microphone device found")?; + let name = get_device_name(&dev); + let (stream, sr, ch) = build_stream(&dev, sender)?; + println!("Audio capture mode: MICROPHONE ONLY [{}] ({} Hz, {} ch)", name, sr, ch); + Ok((Self { _streams: vec![stream] }, sr, ch)) + } + "both" => { + let sys_dev = find_system_device().ok_or("No system audio monitor found")?; + let mic_dev = find_mic_device().ok_or("No microphone found")?; + + let sys_name = get_device_name(&sys_dev); + let mic_name = get_device_name(&mic_dev); + + let (sys_tx, sys_rx) = crossbeam_channel::bounded::>(100); + let (mic_tx, mic_rx) = crossbeam_channel::bounded::>(100); + + let (sys_stream, sr, ch) = build_stream(&sys_dev, sys_tx)?; + let (mic_stream, _, _) = build_stream(&mic_dev, mic_tx)?; + + let out_tx = sender; + thread::spawn(move || { + let mut sys_q: VecDeque = VecDeque::with_capacity(16384); + let mut mic_q: VecDeque = VecDeque::with_capacity(16384); + + loop { + while let Ok(chunk) = sys_rx.try_recv() { + sys_q.extend(chunk); + } + while let Ok(chunk) = mic_rx.try_recv() { + mic_q.extend(chunk); + } + + let avail = sys_q.len().min(mic_q.len()); + if avail >= 480 { + let mut mixed = Vec::with_capacity(avail); + for _ in 0..avail { + let s = sys_q.pop_front().unwrap_or(0.0); + let m = mic_q.pop_front().unwrap_or(0.0); + mixed.push((s + m * 0.9).clamp(-1.0, 1.0)); + } + let _ = out_tx.try_send(mixed); + } else if sys_q.len() > 4800 { + let chunk: Vec = sys_q.drain(..960.min(sys_q.len())).collect(); + let _ = out_tx.try_send(chunk); + } else if mic_q.len() > 4800 { + let chunk: Vec = mic_q.drain(..960.min(mic_q.len())).collect(); + let _ = out_tx.try_send(chunk); + } + + thread::sleep(std::time::Duration::from_millis(4)); + } + }); + + println!("Audio capture mode: BOTH (System [{}] + Mic [{}]) ({} Hz, {} ch)", sys_name, mic_name, sr, ch); + Ok((Self { _streams: vec![sys_stream, mic_stream] }, sr, ch)) + } + _ => { + // "system" is default + let dev = find_system_device().ok_or("No audio device available")?; + let name = get_device_name(&dev); + let (stream, sr, ch) = build_stream(&dev, sender)?; + println!("Audio capture mode: SYSTEM SOUNDS ONLY [{}] ({} Hz, {} ch)", name, sr, ch); + Ok((Self { _streams: vec![stream] }, sr, ch)) + } + } } } diff --git a/src/config.rs b/src/config.rs index d65809e..f6a6b02 100644 --- a/src/config.rs +++ b/src/config.rs @@ -2,24 +2,55 @@ use serde::{Deserialize, Serialize}; use std::fs; use std::path::PathBuf; +fn default_true() -> bool { true } +fn default_replay_duration() -> u32 { 60 } +fn default_replay_bitrate() -> u32 { 18000 } +fn default_record_bitrate() -> u32 { 20000 } +fn default_fps() -> u32 { 60 } +fn default_video_codec() -> String { "h264".to_string() } +fn default_audio_device() -> String { "default".to_string() } +fn default_audio_mode() -> String { "system".to_string() } +fn default_lang() -> String { "en".to_string() } +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() } + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct VrecConfig { + #[serde(default = "default_true")] pub replay_enabled: bool, + #[serde(default = "default_replay_duration")] pub replay_duration_sec: u32, + #[serde(default = "default_replay_bitrate")] pub replay_bitrate_kbps: u32, + #[serde(default)] pub record_enabled: bool, + #[serde(default = "default_record_bitrate")] pub record_bitrate_kbps: u32, + #[serde(default = "default_fps")] pub fps: u32, + #[serde(default = "default_video_codec")] pub video_codec: String, + #[serde(default = "VrecConfig::default_output_directory")] pub output_directory: String, + #[serde(default = "default_audio_device")] pub audio_device: String, + #[serde(default = "default_audio_mode")] + pub audio_mode: String, + #[serde(default)] pub autostart: bool, + #[serde(default = "default_lang")] pub language: String, + #[serde(default = "default_theme")] pub ui_color_theme: String, + #[serde(default = "default_save_hotkey")] pub save_hotkey: String, + #[serde(default = "default_menu_hotkey")] pub menu_hotkey: String, + #[serde(default = "default_record_hotkey")] pub record_hotkey: String, } @@ -36,6 +67,7 @@ impl Default for VrecConfig { video_codec: "h264".to_string(), output_directory: Self::default_output_directory(), audio_device: "default".to_string(), + audio_mode: "system".to_string(), autostart: false, language: "en".to_string(), @@ -61,52 +93,31 @@ impl VrecConfig { path } + pub fn resolve_save_path(&self, filename: &str) -> String { + let mut p = PathBuf::from(&self.output_directory); + p.push(filename); + p.to_string_lossy().to_string() + } + pub fn load() -> Self { let path = Self::config_path(); - if path.exists() - && let Ok(data) = fs::read_to_string(&path) - && let Ok(config) = serde_json::from_str(&data) { - return config; + if let Ok(content) = fs::read_to_string(&path) { + if let Ok(cfg) = serde_json::from_str(&content) { + return cfg; } - Self::default() + } + let default_cfg = Self::default(); + let _ = default_cfg.save(); + default_cfg } - pub fn resolve_save_path(&self, filename: &str) -> String { - let p = std::path::Path::new(&self.output_directory); - let _ = fs::create_dir_all(p); - p.join(filename).to_string_lossy().to_string() - } - - pub fn save(&self) -> Result<(), std::io::Error> { + pub fn save(&self) -> Result<(), Box> { let path = Self::config_path(); if let Some(parent) = path.parent() { fs::create_dir_all(parent)?; } - let _ = fs::create_dir_all(&self.output_directory); - let data = serde_json::to_string_pretty(self)?; - fs::write(path, data)?; - let _ = self.update_autostart(); - Ok(()) - } - - pub fn update_autostart(&self) -> Result<(), std::io::Error> { - let mut autostart_dir = dirs::config_dir().unwrap_or_else(|| PathBuf::from(".")); - autostart_dir.push("autostart"); - let desktop_file = autostart_dir.join("vrec.desktop"); - - if self.autostart { - fs::create_dir_all(&autostart_dir)?; - let content = "[Desktop Entry]\n\ - Type=Application\n\ - Name=vrec Screen Recorder\n\ - Comment=Hardware-accelerated screen recorder and instant replay\n\ - Exec=sh -c \"vrec-daemon & vrec-ui\"\n\ - Terminal=false\n\ - Categories=AudioVideo;Recorder;\n"; - fs::write(desktop_file, content)?; - } else if desktop_file.exists() { - let _ = fs::remove_file(desktop_file); - } + let json = serde_json::to_string_pretty(self)?; + fs::write(path, json)?; Ok(()) } @@ -122,13 +133,14 @@ mod tests { #[test] fn test_config_defaults_and_json() { let cfg = VrecConfig::default(); - assert!(cfg.replay_enabled); assert_eq!(cfg.replay_duration_sec, 60); + assert_eq!(cfg.replay_bitrate_kbps, 18000); assert_eq!(cfg.fps, 60); - assert!(cfg.output_directory.contains("vrec")); + assert_eq!(cfg.video_codec, "h264"); + assert_eq!(cfg.audio_mode, "system"); let json = serde_json::to_string(&cfg).unwrap(); let parsed: VrecConfig = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.menu_hotkey, "Alt+Z"); - assert_eq!(parsed.save_hotkey, "Ctrl+Shift+R"); + assert_eq!(parsed.replay_duration_sec, 60); + assert_eq!(parsed.audio_mode, "system"); } } diff --git a/src/ipc.rs b/src/ipc.rs index ebf75d5..41086e3 100644 --- a/src/ipc.rs +++ b/src/ipc.rs @@ -7,6 +7,7 @@ pub enum Command { StopRecording, ToggleRecording, ToggleAudio, + CycleAudioMode, ReloadConfig, StopDaemon, ShowOverlay, @@ -19,6 +20,12 @@ pub struct DaemonStatus { pub recording_duration_sec: u64, pub is_replay_active: bool, pub audio_muted: bool, + #[serde(default = "default_audio_mode_str")] + pub audio_mode: String, +} + +fn default_audio_mode_str() -> String { + "system".to_string() } pub fn send_command(cmd: Command) -> Result<(), Box> { diff --git a/src/overlay.rs b/src/overlay.rs index df6a9d6..c3fab3b 100644 --- a/src/overlay.rs +++ b/src/overlay.rs @@ -1,7 +1,7 @@ use gtk::prelude::*; use gtk::{ Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label, - Orientation, Stack, StackTransitionType, StyleContext, Switch, + Orientation, SpinButton, Stack, StackTransitionType, StyleContext, Switch, }; #[cfg(target_os = "linux")] use gtk_layer_shell::{Layer, LayerShell}; @@ -32,8 +32,8 @@ pub fn show_notification(message: &str) { app.connect_activate(move |app| { let window = ApplicationWindow::builder() .application(app) - .default_width(340) - .default_height(70) + .default_width(320) + .default_height(60) .build(); #[cfg(target_os = "linux")] @@ -41,7 +41,7 @@ pub fn show_notification(message: &str) { window.init_layer_shell(); window.set_layer(Layer::Overlay); window.set_namespace("vrec-notification"); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 35); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30); window.set_anchor(gtk_layer_shell::Edge::Top, true); } @@ -56,16 +56,16 @@ pub fn show_notification(message: &str) { let css_provider = CssProvider::new(); let css = r#" window { - background-color: rgba(18, 20, 26, 0.95); - border-radius: 16px; - border: 1px solid rgba(255, 255, 255, 0.18); - box-shadow: 0px 8px 30px rgba(0, 0, 0, 0.7); + background-color: #12141a; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.15); + box-shadow: 0px 8px 32px rgba(0, 0, 0, 0.8); } label { color: #ffffff; - font-weight: 800; - font-size: 16px; - padding: 12px 28px; + font-weight: 700; + font-size: 14px; + padding: 10px 24px; } "#; let _ = css_provider.load_from_data(css.as_bytes()); @@ -111,8 +111,8 @@ pub fn show_menu_overlay() { let window = ApplicationWindow::builder() .application(app) - .default_width(860) - .default_height(470) + .default_width(820) + .default_height(480) .build(); #[cfg(target_os = "linux")] @@ -121,7 +121,7 @@ pub fn show_menu_overlay() { window.set_layer(Layer::Overlay); window.set_namespace("vrec-hud"); window.set_keyboard_interactivity(true); - window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 70); + window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 50); window.set_anchor(gtk_layer_shell::Edge::Top, true); } @@ -133,25 +133,24 @@ pub fn show_menu_overlay() { window.set_position(gtk::WindowPosition::Center); } - // Main Stack (Allows toggling between HUD cards and Inline Settings with NO extra windows!) let stack = Stack::new(); stack.set_transition_type(StackTransitionType::Crossfade); - stack.set_transition_duration(200); + stack.set_transition_duration(180); // ========================================== - // PAGE 1: HUD Quick Actions (Xbox / GPU Screen Recorder layout) + // PAGE 1: HUD Quick Actions (GPU Screen Recorder Style) // ========================================== let hud_page = Box::new(Orientation::Vertical, 16); hud_page.set_margin_start(24); hud_page.set_margin_end(24); - hud_page.set_margin_top(20); - hud_page.set_margin_bottom(20); + hud_page.set_margin_top(18); + hud_page.set_margin_bottom(18); - // Header + // Header Bar let header_box = Box::new(Orientation::Horizontal, 10); - let title_label = Label::new(Some("vrec")); + let title_label = Label::new(Some("VREC")); title_label.style_context().add_class("hud-title"); - let badge_label = Label::new(Some("GPU ZERO-COPY")); + let badge_label = Label::new(Some("GPU ACCELERATED")); badge_label.style_context().add_class("badge-tag"); let spacer = Box::new(Orientation::Horizontal, 0); let close_btn = Button::with_label("✕"); @@ -162,61 +161,120 @@ pub fn show_menu_overlay() { header_box.pack_start(&spacer, true, true, 0); header_box.pack_start(&close_btn, false, false, 0); - // Cards Row (4 Primary Action Cards) - let cards_box = Box::new(Orientation::Horizontal, 16); + // 4 Action Cards Row + let cards_box = Box::new(Orientation::Horizontal, 14); cards_box.set_halign(gtk::Align::Center); - // Card 1: Instant Replay + // Card 1: Replay let replay_card = Box::new(Orientation::Vertical, 8); replay_card.style_context().add_class("card-box"); - let replay_toggle_btn = Button::with_label(if config.replay_enabled { - "Instant Replay\nEnabled" - } else { - "Instant Replay\nDisabled" - }); - replay_toggle_btn.set_size_request(170, 75); + let replay_hdr = Box::new(Orientation::Horizontal, 6); + let replay_title = Label::new(Some("REPLAY")); + replay_title.style_context().add_class("card-header-label"); + let replay_pill = Label::new(Some(if config.replay_enabled { "ACTIVE" } else { "OFF" })); + replay_pill.style_context().add_class("status-pill"); + replay_pill.style_context().add_class(if config.replay_enabled { "pill-active" } else { "pill-idle" }); + let replay_sp = Box::new(Orientation::Horizontal, 0); + replay_hdr.pack_start(&replay_title, false, false, 0); + replay_hdr.pack_start(&replay_sp, true, true, 0); + replay_hdr.pack_start(&replay_pill, false, false, 0); + + let replay_toggle_btn = Button::with_label(if config.replay_enabled { "Disable Replay" } else { "Enable Replay" }); + replay_toggle_btn.set_size_request(165, 45); replay_toggle_btn.style_context().add_class("action-btn"); let replay_save_btn = Button::with_label("Save Replay"); replay_save_btn.style_context().add_class("sub-btn"); - replay_card.pack_start(&replay_toggle_btn, true, true, 0); + replay_card.pack_start(&replay_hdr, false, false, 0); + replay_card.pack_start(&replay_toggle_btn, true, true, 4); replay_card.pack_start(&replay_save_btn, false, false, 0); - // Card 2: Normal Recording + // Card 2: Record let record_card = Box::new(Orientation::Vertical, 8); record_card.style_context().add_class("card-box"); - let record_toggle_btn = Button::with_label("Record\nStart"); - record_toggle_btn.set_size_request(170, 75); + let record_hdr = Box::new(Orientation::Horizontal, 6); + let record_title = Label::new(Some("RECORD")); + record_title.style_context().add_class("card-header-label"); + let record_pill = Label::new(Some("IDLE")); + record_pill.style_context().add_class("status-pill"); + record_pill.style_context().add_class("pill-idle"); + let record_sp = Box::new(Orientation::Horizontal, 0); + record_hdr.pack_start(&record_title, false, false, 0); + record_hdr.pack_start(&record_sp, true, true, 0); + record_hdr.pack_start(&record_pill, false, false, 0); + + let record_toggle_btn = Button::with_label("Start Record"); + record_toggle_btn.set_size_request(165, 45); record_toggle_btn.style_context().add_class("action-btn"); - let record_status_sub = Label::new(Some("H.264 • 60 FPS")); - record_status_sub.style_context().add_class("sub-label"); - record_card.pack_start(&record_toggle_btn, true, true, 0); - record_card.pack_start(&record_status_sub, false, false, 0); + let record_format_sub = Label::new(Some(&format!("{} • {} FPS", config.video_codec.to_uppercase(), config.fps))); + record_format_sub.style_context().add_class("sub-label"); + record_card.pack_start(&record_hdr, false, false, 0); + record_card.pack_start(&record_toggle_btn, true, true, 4); + record_card.pack_start(&record_format_sub, false, false, 0); - // Card 3: Microphone & Audio + // Card 3: Audio Mode let audio_card = Box::new(Orientation::Vertical, 8); audio_card.style_context().add_class("card-box"); - let mic_toggle_btn = Button::with_label("Microphone\nActive"); - mic_toggle_btn.set_size_request(170, 75); - mic_toggle_btn.style_context().add_class("action-btn"); + let audio_hdr = Box::new(Orientation::Horizontal, 6); + let audio_title = Label::new(Some("AUDIO")); + audio_title.style_context().add_class("card-header-label"); + let audio_pill = Label::new(Some(match config.audio_mode.as_str() { + "mic" => "MIC ONLY", + "both" => "SYSTEM + MIC", + "muted" => "MUTED", + _ => "SYSTEM ONLY", + })); + audio_pill.style_context().add_class("status-pill"); + audio_pill.style_context().add_class(match config.audio_mode.as_str() { + "mic" => "pill-cyan", + "both" => "pill-purple", + "muted" => "pill-recording", + _ => "pill-active", + }); + let audio_sp = Box::new(Orientation::Horizontal, 0); + audio_hdr.pack_start(&audio_title, false, false, 0); + audio_hdr.pack_start(&audio_sp, true, true, 0); + audio_hdr.pack_start(&audio_pill, false, false, 0); - let mic_status_sub = Label::new(Some("AAC • 48 kHz")); - mic_status_sub.style_context().add_class("sub-label"); - audio_card.pack_start(&mic_toggle_btn, true, true, 0); - audio_card.pack_start(&mic_status_sub, false, false, 0); + let audio_cycle_btn = Button::with_label(match config.audio_mode.as_str() { + "mic" => "Microphone Only", + "both" => "System + Mic", + "muted" => "Muted", + _ => "System Audio Only", + }); + audio_cycle_btn.set_size_request(165, 45); + audio_cycle_btn.style_context().add_class("action-btn"); + + let audio_hint_sub = Label::new(Some("Click to Cycle Mode")); + audio_hint_sub.style_context().add_class("sub-label"); + audio_card.pack_start(&audio_hdr, false, false, 0); + audio_card.pack_start(&audio_cycle_btn, true, true, 4); + audio_card.pack_start(&audio_hint_sub, false, false, 0); // Card 4: Settings let settings_card = Box::new(Orientation::Vertical, 8); settings_card.style_context().add_class("card-box"); - let settings_open_btn = Button::with_label("Settings\nConfigure"); - settings_open_btn.set_size_request(170, 75); + let settings_hdr = Box::new(Orientation::Horizontal, 6); + let settings_title_hdr = Label::new(Some("PREFERENCES")); + settings_title_hdr.style_context().add_class("card-header-label"); + let settings_pill = Label::new(Some("TUNING")); + settings_pill.style_context().add_class("status-pill"); + settings_pill.style_context().add_class("pill-idle"); + let settings_sp = Box::new(Orientation::Horizontal, 0); + settings_hdr.pack_start(&settings_title_hdr, false, false, 0); + settings_hdr.pack_start(&settings_sp, true, true, 0); + settings_hdr.pack_start(&settings_pill, false, false, 0); + + let settings_open_btn = Button::with_label("Open Settings"); + settings_open_btn.set_size_request(165, 45); settings_open_btn.style_context().add_class("action-btn"); - let shortcut_hint = Label::new(Some("Esc to Close")); - shortcut_hint.style_context().add_class("sub-label"); - settings_card.pack_start(&settings_open_btn, true, true, 0); - settings_card.pack_start(&shortcut_hint, false, false, 0); + let settings_hint_sub = Label::new(Some("Esc to Close")); + settings_hint_sub.style_context().add_class("sub-label"); + settings_card.pack_start(&settings_hdr, false, false, 0); + settings_card.pack_start(&settings_open_btn, true, true, 4); + settings_card.pack_start(&settings_hint_sub, false, false, 0); cards_box.pack_start(&replay_card, true, true, 0); cards_box.pack_start(&record_card, true, true, 0); @@ -227,75 +285,90 @@ pub fn show_menu_overlay() { hud_page.pack_start(&cards_box, true, true, 0); // ========================================== - // PAGE 2: Inline Settings Panel (Zero separate windows!) + // PAGE 2: Fine-Tuned Settings Panel (Numeric inputs for Time, Quality, FPS) // ========================================== - let settings_page = Box::new(Orientation::Vertical, 14); - settings_page.set_margin_start(30); - settings_page.set_margin_end(30); - settings_page.set_margin_top(20); - settings_page.set_margin_bottom(20); + let settings_page = Box::new(Orientation::Vertical, 12); + settings_page.set_margin_start(24); + settings_page.set_margin_end(24); + settings_page.set_margin_top(16); + settings_page.set_margin_bottom(16); let settings_header = Box::new(Orientation::Horizontal, 12); - let back_btn = Button::with_label("Back"); + let back_btn = Button::with_label("Back to HUD"); back_btn.style_context().add_class("sub-btn"); - let settings_title = Label::new(Some("Recorder Settings")); + let settings_title = Label::new(Some("Recorder Settings & Quality Tuning")); settings_title.style_context().add_class("hud-title"); settings_header.pack_start(&back_btn, false, false, 0); settings_header.pack_start(&settings_title, false, false, 0); let settings_grid = gtk::Grid::new(); - settings_grid.set_column_spacing(24); - settings_grid.set_row_spacing(12); + settings_grid.set_column_spacing(20); + settings_grid.set_row_spacing(10); - // Replay Duration - let dur_lbl = Label::new(Some("Replay Duration:")); + // 1. Replay Duration (Seconds) - Direct numeric spin button + preset pills! + let dur_lbl = Label::new(Some("Replay Buffer (Seconds):")); dur_lbl.set_halign(gtk::Align::Start); - let dur_combo = ComboBoxText::new(); - dur_combo.append(Some("15"), "15 Seconds"); - dur_combo.append(Some("30"), "30 Seconds"); - dur_combo.append(Some("60"), "1 Minute"); - dur_combo.append(Some("120"), "2 Minutes"); - dur_combo.append(Some("300"), "5 Minutes"); - dur_combo.append(Some("600"), "10 Minutes"); - dur_combo.set_active_id(Some(&config.replay_duration_sec.to_string())); + let dur_box = Box::new(Orientation::Horizontal, 6); + let dur_spin = SpinButton::with_range(5.0, 1800.0, 5.0); + dur_spin.set_value(config.replay_duration_sec as f64); + dur_box.pack_start(&dur_spin, false, false, 0); + + let p_30s = Button::with_label("30s"); p_30s.style_context().add_class("preset-btn"); + let p_60s = Button::with_label("60s"); p_60s.style_context().add_class("preset-btn"); + let p_2m = Button::with_label("2m"); p_2m.style_context().add_class("preset-btn"); + let p_5m = Button::with_label("5m"); p_5m.style_context().add_class("preset-btn"); + let s_clone = dur_spin.clone(); p_30s.connect_clicked(move |_| s_clone.set_value(30.0)); + let s_clone = dur_spin.clone(); p_60s.connect_clicked(move |_| s_clone.set_value(60.0)); + let s_clone = dur_spin.clone(); p_2m.connect_clicked(move |_| s_clone.set_value(120.0)); + let s_clone = dur_spin.clone(); p_5m.connect_clicked(move |_| s_clone.set_value(300.0)); + dur_box.pack_start(&p_30s, false, false, 0); + dur_box.pack_start(&p_60s, false, false, 0); + dur_box.pack_start(&p_2m, false, false, 0); + dur_box.pack_start(&p_5m, false, false, 0); - // Video Bitrate - let bit_lbl = Label::new(Some("Recording Quality:")); + // 2. Video Quality / Bitrate (Mbps) - Direct numeric spin button + preset pills! + let bit_lbl = Label::new(Some("Video Bitrate (Mbps):")); bit_lbl.set_halign(gtk::Align::Start); - let bit_combo = ComboBoxText::new(); - bit_combo.append(Some("10000"), "Low (10 Mbps)"); - bit_combo.append(Some("15000"), "Medium (15 Mbps)"); - bit_combo.append(Some("25000"), "High (25 Mbps)"); - bit_combo.append(Some("50000"), "Ultra (50 Mbps)"); - bit_combo.set_active_id(Some(&config.record_bitrate_kbps.to_string())); + let bit_box = Box::new(Orientation::Horizontal, 6); + let bit_spin = SpinButton::with_range(2.0, 100.0, 1.0); + bit_spin.set_value((config.record_bitrate_kbps / 1000) as f64); + bit_box.pack_start(&bit_spin, false, false, 0); - // Autostart - let auto_lbl = Label::new(Some("Autostart on Boot:")); - auto_lbl.set_halign(gtk::Align::Start); - let auto_switch = Switch::new(); - auto_switch.set_active(config.autostart); + let b_10m = Button::with_label("10M"); b_10m.style_context().add_class("preset-btn"); + let b_20m = Button::with_label("20M"); b_20m.style_context().add_class("preset-btn"); + let b_30m = Button::with_label("30M"); b_30m.style_context().add_class("preset-btn"); + let b_50m = Button::with_label("50M"); b_50m.style_context().add_class("preset-btn"); + let b_clone = bit_spin.clone(); b_10m.connect_clicked(move |_| b_clone.set_value(10.0)); + let b_clone = bit_spin.clone(); b_20m.connect_clicked(move |_| b_clone.set_value(20.0)); + let b_clone = bit_spin.clone(); b_30m.connect_clicked(move |_| b_clone.set_value(30.0)); + let b_clone = bit_spin.clone(); b_50m.connect_clicked(move |_| b_clone.set_value(50.0)); + bit_box.pack_start(&b_10m, false, false, 0); + bit_box.pack_start(&b_20m, false, false, 0); + bit_box.pack_start(&b_30m, false, false, 0); + bit_box.pack_start(&b_50m, false, false, 0); - // Hotkeys - let save_hk_lbl = Label::new(Some("Save Replay Hotkey:")); - save_hk_lbl.set_halign(gtk::Align::Start); - let save_hk_entry = Entry::new(); - save_hk_entry.set_text(&config.save_hotkey); - - let menu_hk_lbl = Label::new(Some("Menu Overlay Hotkey:")); - menu_hk_lbl.set_halign(gtk::Align::Start); - let menu_hk_entry = Entry::new(); - menu_hk_entry.set_text(&config.menu_hotkey); - - // Framerate Selector (Crucial for low-end PCs: 30 FPS cuts GPU load by 50%) - let fps_lbl = Label::new(Some("Framerate (FPS):")); + // 3. Framerate (FPS) - Direct numeric spin button + preset pills! + let fps_lbl = Label::new(Some("Target Framerate (FPS):")); fps_lbl.set_halign(gtk::Align::Start); - let fps_combo = ComboBoxText::new(); - fps_combo.append(Some("30"), "30 FPS (Low-End PC)"); - fps_combo.append(Some("60"), "60 FPS (Smooth / Gaming)"); - fps_combo.append(Some("120"), "120 FPS (High-End)"); - fps_combo.set_active_id(Some(&config.fps.to_string())); + let fps_box = Box::new(Orientation::Horizontal, 6); + let fps_spin = SpinButton::with_range(15.0, 240.0, 1.0); + fps_spin.set_value(config.fps as f64); + fps_box.pack_start(&fps_spin, false, false, 0); - // Video Codec Selector + let f_30 = Button::with_label("30"); f_30.style_context().add_class("preset-btn"); + let f_60 = Button::with_label("60"); f_60.style_context().add_class("preset-btn"); + let f_120 = Button::with_label("120"); f_120.style_context().add_class("preset-btn"); + let f_144 = Button::with_label("144"); f_144.style_context().add_class("preset-btn"); + let f_clone = fps_spin.clone(); f_30.connect_clicked(move |_| f_clone.set_value(30.0)); + let f_clone = fps_spin.clone(); f_60.connect_clicked(move |_| f_clone.set_value(60.0)); + let f_clone = fps_spin.clone(); f_120.connect_clicked(move |_| f_clone.set_value(120.0)); + let f_clone = fps_spin.clone(); f_144.connect_clicked(move |_| f_clone.set_value(144.0)); + fps_box.pack_start(&f_30, false, false, 0); + fps_box.pack_start(&f_60, false, false, 0); + fps_box.pack_start(&f_120, false, false, 0); + fps_box.pack_start(&f_144, false, false, 0); + + // 4. Video Codec let codec_lbl = Label::new(Some("Video Codec:")); codec_lbl.set_halign(gtk::Align::Start); let codec_combo = ComboBoxText::new(); @@ -304,8 +377,18 @@ pub fn show_menu_overlay() { codec_combo.append(Some("av1"), "AV1 (Next-Gen High Quality)"); codec_combo.set_active_id(Some(&config.video_codec)); - // Audio Input Device Selector - let audio_dev_lbl = Label::new(Some("Audio Device:")); + // 5. Audio Source Mode (System Only vs Mic Only vs Both!) + let audio_mode_lbl = Label::new(Some("Audio Track Mode:")); + audio_mode_lbl.set_halign(gtk::Align::Start); + let audio_mode_combo = ComboBoxText::new(); + audio_mode_combo.append(Some("system"), "System Sounds Only (Game / Desktop)"); + audio_mode_combo.append(Some("mic"), "Microphone Only"); + audio_mode_combo.append(Some("both"), "Both (System Sounds + Microphone Merged)"); + audio_mode_combo.append(Some("muted"), "Muted (No Audio)"); + audio_mode_combo.set_active_id(Some(&config.audio_mode)); + + // 6. Audio Device + let audio_dev_lbl = Label::new(Some("Audio Hardware Device:")); audio_dev_lbl.set_halign(gtk::Align::Start); let audio_dev_combo = ComboBoxText::new(); audio_dev_combo.append(Some("default"), "Default Device"); @@ -314,39 +397,58 @@ pub fn show_menu_overlay() { } audio_dev_combo.set_active_id(Some(&config.audio_device)); - // Save Folder + // 7. Save Folder let dir_lbl = Label::new(Some("Save Folder:")); dir_lbl.set_halign(gtk::Align::Start); let dir_entry = Entry::new(); dir_entry.set_text(&config.output_directory); + // 8. Hotkeys + let save_hk_lbl = Label::new(Some("Save Replay Hotkey:")); + save_hk_lbl.set_halign(gtk::Align::Start); + let save_hk_entry = Entry::new(); + save_hk_entry.set_text(&config.save_hotkey); + let record_hk_lbl = Label::new(Some("Toggle Record Hotkey:")); record_hk_lbl.set_halign(gtk::Align::Start); let record_hk_entry = Entry::new(); record_hk_entry.set_text(&config.record_hotkey); + let menu_hk_lbl = Label::new(Some("Menu Overlay Hotkey:")); + menu_hk_lbl.set_halign(gtk::Align::Start); + let menu_hk_entry = Entry::new(); + menu_hk_entry.set_text(&config.menu_hotkey); + + // 9. Autostart + let auto_lbl = Label::new(Some("Autostart on Login:")); + auto_lbl.set_halign(gtk::Align::Start); + let auto_switch = Switch::new(); + auto_switch.set_active(config.autostart); + settings_grid.attach(&dur_lbl, 0, 0, 1, 1); - settings_grid.attach(&dur_combo, 1, 0, 1, 1); + settings_grid.attach(&dur_box, 1, 0, 1, 1); settings_grid.attach(&bit_lbl, 0, 1, 1, 1); - settings_grid.attach(&bit_combo, 1, 1, 1, 1); + settings_grid.attach(&bit_box, 1, 1, 1, 1); settings_grid.attach(&fps_lbl, 0, 2, 1, 1); - settings_grid.attach(&fps_combo, 1, 2, 1, 1); + settings_grid.attach(&fps_box, 1, 2, 1, 1); settings_grid.attach(&codec_lbl, 0, 3, 1, 1); settings_grid.attach(&codec_combo, 1, 3, 1, 1); - settings_grid.attach(&audio_dev_lbl, 0, 4, 1, 1); - settings_grid.attach(&audio_dev_combo, 1, 4, 1, 1); - settings_grid.attach(&dir_lbl, 0, 5, 1, 1); - settings_grid.attach(&dir_entry, 1, 5, 1, 1); - settings_grid.attach(&auto_lbl, 0, 6, 1, 1); - settings_grid.attach(&auto_switch, 1, 6, 1, 1); + settings_grid.attach(&audio_mode_lbl, 0, 4, 1, 1); + settings_grid.attach(&audio_mode_combo, 1, 4, 1, 1); + settings_grid.attach(&audio_dev_lbl, 0, 5, 1, 1); + settings_grid.attach(&audio_dev_combo, 1, 5, 1, 1); + settings_grid.attach(&dir_lbl, 0, 6, 1, 1); + settings_grid.attach(&dir_entry, 1, 6, 1, 1); settings_grid.attach(&save_hk_lbl, 0, 7, 1, 1); settings_grid.attach(&save_hk_entry, 1, 7, 1, 1); settings_grid.attach(&record_hk_lbl, 0, 8, 1, 1); settings_grid.attach(&record_hk_entry, 1, 8, 1, 1); settings_grid.attach(&menu_hk_lbl, 0, 9, 1, 1); settings_grid.attach(&menu_hk_entry, 1, 9, 1, 1); + settings_grid.attach(&auto_lbl, 0, 10, 1, 1); + settings_grid.attach(&auto_switch, 1, 10, 1, 1); - let save_settings_btn = Button::with_label("Apply & Save"); + let save_settings_btn = Button::with_label("Apply & Save Settings"); save_settings_btn.style_context().add_class("apply-btn"); settings_page.pack_start(&settings_header, false, false, 0); @@ -357,75 +459,133 @@ pub fn show_menu_overlay() { stack.add_named(&settings_page, "settings"); window.add(&stack); - // CSS Styling (Sleek Frosted Glass HUD) + // CSS Styling (Brushed Stealth HUD Theme) let css_provider = CssProvider::new(); let css = r#" window { - background-color: rgba(18, 20, 26, 0.95); - border-radius: 20px; - border: 1px solid rgba(255, 255, 255, 0.12); - box-shadow: 0px 12px 40px rgba(0, 0, 0, 0.75); - color: #ffffff; + background-color: #12141a; + border-radius: 16px; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 16px 48px rgba(0, 0, 0, 0.85); + color: #f1f5f9; } .hud-title { - font-size: 20px; - font-weight: 900; + font-size: 18px; + font-weight: 800; color: #ffffff; - letter-spacing: 1px; + letter-spacing: 1.5px; } .badge-tag { - background-color: rgba(56, 189, 248, 0.2); + background-color: rgba(56, 189, 248, 0.12); color: #38bdf8; + border: 1px solid rgba(56, 189, 248, 0.3); font-size: 10px; - font-weight: bold; + font-weight: 700; border-radius: 6px; padding: 2px 8px; } .card-box { - background-color: rgba(255, 255, 255, 0.04); - border: 1px solid rgba(255, 255, 255, 0.08); - border-radius: 14px; - padding: 10px; + background-color: #181b23; + border: 1px solid rgba(255, 255, 255, 0.07); + border-radius: 12px; + padding: 12px; + min-width: 170px; } .card-box:hover { - background-color: rgba(255, 255, 255, 0.07); - border-color: rgba(255, 255, 255, 0.18); + background-color: #1e222d; + border-color: rgba(255, 255, 255, 0.15); + } + .card-header-label { + font-size: 11px; + font-weight: 700; + color: #94a3b8; + letter-spacing: 0.8px; + } + .status-pill { + font-size: 10px; + font-weight: 800; + padding: 2px 8px; + border-radius: 10px; + } + .pill-active { + background-color: rgba(16, 185, 129, 0.15); + color: #10b981; + border: 1px solid rgba(16, 185, 129, 0.3); + } + .pill-idle { + background-color: rgba(148, 163, 184, 0.12); + color: #94a3b8; + border: 1px solid rgba(148, 163, 184, 0.2); + } + .pill-recording { + background-color: rgba(239, 68, 68, 0.2); + color: #ef4444; + border: 1px solid rgba(239, 68, 68, 0.4); + } + .pill-cyan { + background-color: rgba(6, 182, 212, 0.15); + color: #06b6d4; + border: 1px solid rgba(6, 182, 212, 0.3); + } + .pill-purple { + background-color: rgba(168, 85, 247, 0.15); + color: #a855f7; + border: 1px solid rgba(168, 85, 247, 0.3); } .action-btn { - background-color: transparent; - border: none; - color: #f1f5f9; - font-size: 15px; - font-weight: bold; - } - .action-btn:hover { - color: #38bdf8; - } - .recording-active { - color: #ef4444 !important; - animation: pulse 1s infinite alternate; - } - .sub-btn { - background-color: rgba(255, 255, 255, 0.08); - color: #cbd5e1; + background-color: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; - font-size: 12px; + color: #ffffff; + font-size: 13px; + font-weight: 700; + padding: 8px 12px; + } + .action-btn:hover { + background-color: #2563eb; + border-color: #3b82f6; + color: #ffffff; + } + .recording-active { + background-color: rgba(239, 68, 68, 0.25) !important; + border-color: #ef4444 !important; + color: #ef4444 !important; + } + .sub-btn { + background-color: transparent; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + color: #94a3b8; + font-size: 11px; font-weight: 600; - padding: 6px 12px; + padding: 5px 10px; } .sub-btn:hover { - background-color: rgba(255, 255, 255, 0.16); + background-color: rgba(255, 255, 255, 0.08); + color: #ffffff; + } + .preset-btn { + background-color: #1e222d; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + color: #94a3b8; + font-size: 11px; + font-weight: 600; + padding: 4px 8px; + } + .preset-btn:hover { + background-color: rgba(37, 99, 235, 0.2); + border-color: #3b82f6; color: #ffffff; } .apply-btn { background-color: #2563eb; color: #ffffff; border: none; - border-radius: 10px; - font-size: 14px; - font-weight: bold; - padding: 10px 20px; + border-radius: 8px; + font-size: 13px; + font-weight: 700; + padding: 10px 24px; } .apply-btn:hover { background-color: #1d4ed8; @@ -434,7 +594,7 @@ pub fn show_menu_overlay() { background-color: transparent; color: #94a3b8; border: none; - font-size: 18px; + font-size: 16px; font-weight: bold; padding: 4px 8px; } @@ -446,12 +606,26 @@ pub fn show_menu_overlay() { font-size: 11px; font-weight: 500; } - entry, combobox button { - background-color: rgba(30, 32, 40, 0.9); - color: #ffffff; - border: 1px solid rgba(255, 255, 255, 0.12); + entry, spinbutton, combobox button { + background-color: #171a22; + color: #f1f5f9; + border: 1px solid rgba(255, 255, 255, 0.1); border-radius: 8px; padding: 6px 10px; + font-size: 13px; + font-weight: 600; + } + entry:focus, spinbutton:focus, combobox button:focus { + border-color: #3b82f6; + background-color: #1c202a; + } + spinbutton button { + background-color: transparent; + color: #94a3b8; + border: none; + } + spinbutton button:hover { + color: #ffffff; } "#; let _ = css_provider.load_from_data(css.as_bytes()); @@ -494,29 +668,25 @@ pub fn show_menu_overlay() { let stack_after_save = stack.clone(); save_settings_btn.connect_clicked(move |_| { let mut cfg = VrecConfig::load(); - if let Some(dur_str) = dur_combo.active_id() - && let Ok(dur) = dur_str.parse() { - cfg.replay_duration_sec = dur; - } - if let Some(bit_str) = bit_combo.active_id() - && let Ok(bit) = bit_str.parse() { - cfg.record_bitrate_kbps = bit; - cfg.replay_bitrate_kbps = bit; - } - if let Some(fps_str) = fps_combo.active_id() - && let Ok(fps) = fps_str.parse() { - cfg.fps = fps; - } + cfg.replay_duration_sec = dur_spin.value() as u32; + let bit_mbps = bit_spin.value() as u32; + cfg.record_bitrate_kbps = bit_mbps * 1000; + cfg.replay_bitrate_kbps = bit_mbps * 1000; + cfg.fps = fps_spin.value() as u32; + if let Some(c) = codec_combo.active_id() { cfg.video_codec = c.to_string(); } + if let Some(m) = audio_mode_combo.active_id() { + cfg.audio_mode = m.to_string(); + } + if let Some(dev) = audio_dev_combo.active_id() { + cfg.audio_device = dev.to_string(); + } let dir_val = dir_entry.text().to_string(); if !dir_val.trim().is_empty() { cfg.output_directory = dir_val; } - if let Some(dev) = audio_dev_combo.active_id() { - cfg.audio_device = dev.to_string(); - } cfg.autostart = auto_switch.is_active(); cfg.save_hotkey = save_hk_entry.text().to_string(); cfg.menu_hotkey = menu_hk_entry.text().to_string(); @@ -528,15 +698,22 @@ pub fn show_menu_overlay() { // Action Handlers let replay_lbl_btn = replay_toggle_btn.clone(); + let replay_pill_clone = replay_pill.clone(); replay_toggle_btn.connect_clicked(move |_| { let mut cfg = VrecConfig::load(); cfg.replay_enabled = !cfg.replay_enabled; let _ = cfg.save(); VrecConfig::notify_daemon_reload(); if cfg.replay_enabled { - replay_lbl_btn.set_label("Instant Replay\nEnabled"); + replay_lbl_btn.set_label("Disable Replay"); + replay_pill_clone.set_label("ACTIVE"); + replay_pill_clone.style_context().remove_class("pill-idle"); + replay_pill_clone.style_context().add_class("pill-active"); } else { - replay_lbl_btn.set_label("Instant Replay\nDisabled"); + replay_lbl_btn.set_label("Enable Replay"); + replay_pill_clone.set_label("OFF"); + replay_pill_clone.style_context().remove_class("pill-active"); + replay_pill_clone.style_context().add_class("pill-idle"); } }); @@ -549,14 +726,53 @@ pub fn show_menu_overlay() { let _ = ipc::send_command(Command::ToggleRecording); }); - mic_toggle_btn.connect_clicked(|_| { - let _ = ipc::send_command(Command::ToggleAudio); + // Audio Source Mode Quick-Cycle + let audio_pill_clone = audio_pill.clone(); + let audio_btn_clone = audio_cycle_btn.clone(); + audio_cycle_btn.connect_clicked(move |_| { + let mut cfg = VrecConfig::load(); + let next_mode = match cfg.audio_mode.as_str() { + "system" => "mic", + "mic" => "both", + "both" => "muted", + _ => "system", + }; + cfg.audio_mode = next_mode.to_string(); + let _ = cfg.save(); + VrecConfig::notify_daemon_reload(); + + audio_pill_clone.style_context().remove_class("pill-active"); + audio_pill_clone.style_context().remove_class("pill-cyan"); + audio_pill_clone.style_context().remove_class("pill-purple"); + audio_pill_clone.style_context().remove_class("pill-recording"); + + match next_mode { + "mic" => { + audio_pill_clone.set_label("MIC ONLY"); + audio_pill_clone.style_context().add_class("pill-cyan"); + audio_btn_clone.set_label("Microphone Only"); + } + "both" => { + audio_pill_clone.set_label("SYSTEM + MIC"); + audio_pill_clone.style_context().add_class("pill-purple"); + audio_btn_clone.set_label("System + Mic"); + } + "muted" => { + audio_pill_clone.set_label("MUTED"); + audio_pill_clone.style_context().add_class("pill-recording"); + audio_btn_clone.set_label("Muted"); + } + _ => { + audio_pill_clone.set_label("SYSTEM ONLY"); + audio_pill_clone.style_context().add_class("pill-active"); + audio_btn_clone.set_label("System Audio Only"); + } + } }); - // Periodic Live Status Query from Daemon (Syncs live recording state & timer!) + // Periodic Status Sync (Timer & Live States) let rec_btn_sync = record_toggle_btn.clone(); - let mic_btn_sync = mic_toggle_btn.clone(); - let replay_btn_sync = replay_toggle_btn.clone(); + let rec_pill_sync = record_pill.clone(); let is_running = Arc::new(AtomicBool::new(true)); let is_running_clone = Arc::clone(&is_running); @@ -569,23 +785,17 @@ pub fn show_menu_overlay() { if status.is_recording { let mins = status.recording_duration_sec / 60; let secs = status.recording_duration_sec % 60; - rec_btn_sync.set_label(&format!("Recording\n{:02}:{:02}", mins, secs)); + rec_btn_sync.set_label("Stop Recording"); rec_btn_sync.style_context().add_class("recording-active"); + rec_pill_sync.set_label(&format!("REC {:02}:{:02}", mins, secs)); + rec_pill_sync.style_context().remove_class("pill-idle"); + rec_pill_sync.style_context().add_class("pill-recording"); } else { - rec_btn_sync.set_label("Record\nStart"); + rec_btn_sync.set_label("Start Record"); rec_btn_sync.style_context().remove_class("recording-active"); - } - - if status.audio_muted { - mic_btn_sync.set_label("Microphone\nMuted"); - } else { - mic_btn_sync.set_label("Microphone\nActive"); - } - - if status.is_replay_active { - replay_btn_sync.set_label("Instant Replay\nEnabled"); - } else { - replay_btn_sync.set_label("Instant Replay\nDisabled"); + rec_pill_sync.set_label("IDLE"); + rec_pill_sync.style_context().remove_class("pill-recording"); + rec_pill_sync.style_context().add_class("pill-idle"); } } gtk::glib::ControlFlow::Continue