feat(ui): Transform overlay into true Game Bar HUD with live timer sync, audio controls, and inline settings

This commit is contained in:
katR 2026-09-03 07:34:33 +05:30
parent 3a84ec5a78
commit 45eaf2840d
3 changed files with 491 additions and 126 deletions

View file

@ -2,14 +2,16 @@ use vrec::capture::{Frame, FrameSource};
use vrec::encoder::VaapiEncoder;
use vrec::ring::Packet;
use vrec::muxer::Muxer;
use vrec::ipc::Command;
use vrec::ipc::{Command, DaemonStatus};
use ringbuf::HeapRb;
use ringbuf::traits::{RingBuffer, Consumer, Observer};
use crossbeam_channel::bounded;
use std::thread;
use std::env;
use std::os::unix::net::UnixListener;
use std::io::Read;
use std::io::{Read, Write};
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@ -40,7 +42,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let (mux_tx, mux_rx) = bounded::<Vec<Packet>>(1);
let (audio_tx, audio_rx) = bounded::<Vec<f32>>(100);
let audio_info = vrec::capture::audio::AudioCapture::new(audio_tx).ok();
let is_recording_state = Arc::new(AtomicBool::new(false));
let record_start_state = Arc::new(AtomicU64::new(0));
let replay_enabled_state = Arc::new(AtomicBool::new(true));
let audio_muted_state = Arc::new(AtomicBool::new(false));
let _ = frame_tx.send(first_frame);
let capture_tx = frame_tx.clone();
@ -60,8 +67,15 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
});
let rec_state_clone = Arc::clone(&is_recording_state);
let rec_start_clone = Arc::clone(&record_start_state);
let replay_state_clone = Arc::clone(&replay_enabled_state);
let audio_muted_clone = Arc::clone(&audio_muted_state);
thread::spawn(move || {
let mut config = vrec::config::VrecConfig::load();
replay_state_clone.store(config.replay_enabled, Ordering::SeqCst);
let mut encoder = VaapiEncoder::new_with_bitrate(width, height, config.record_bitrate_kbps)
.expect("Failed to init encoder");
let codec_ctx_ptr = encoder.codec_ctx() as usize;
@ -111,6 +125,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
match cmd {
Command::ReloadConfig => {
config = vrec::config::VrecConfig::load();
replay_state_clone.store(config.replay_enabled, Ordering::SeqCst);
println!("Daemon config reloaded!");
let new_capacity = (config.replay_duration_sec * 120).max(120) as usize;
if ring.capacity().get() != new_capacity {
@ -128,6 +143,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if !normal_recording {
normal_recording = true;
normal_waiting_keyframe = true;
rec_state_clone.store(true, Ordering::SeqCst);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
rec_start_clone.store(now, Ordering::SeqCst);
println!("StartRecording requested, waiting for keyframe...");
}
},
@ -138,6 +156,8 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
normal_recording = false;
normal_waiting_keyframe = false;
rec_state_clone.store(false, Ordering::SeqCst);
rec_start_clone.store(0, Ordering::SeqCst);
},
Command::ToggleRecording => {
if normal_recording {
@ -147,17 +167,32 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
normal_recording = false;
normal_waiting_keyframe = false;
rec_state_clone.store(false, Ordering::SeqCst);
rec_start_clone.store(0, Ordering::SeqCst);
} else {
normal_recording = true;
normal_waiting_keyframe = true;
rec_state_clone.store(true, Ordering::SeqCst);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
rec_start_clone.store(now, Ordering::SeqCst);
println!("ToggleRecording: StartRecording requested, waiting for keyframe...");
}
},
Command::ToggleAudio => {
let cur = audio_muted_clone.load(Ordering::SeqCst);
audio_muted_clone.store(!cur, Ordering::SeqCst);
println!("Audio mute toggled: {}", !cur);
},
_ => {}
}
}
while let Ok(audio_chunk) = audio_rx.try_recv() {
while let Ok(mut audio_chunk) = audio_rx.try_recv() {
if audio_muted_clone.load(Ordering::Relaxed) {
for sample in audio_chunk.iter_mut() {
*sample = 0.0;
}
}
if let Some(enc) = audio_encoder.as_mut()
&& let Ok(audio_packets) = enc.encode_pcm(&audio_chunk) {
for mut pkt in audio_packets {
@ -215,6 +250,27 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if stream.read_exact(&mut payload).is_ok()
&& let Ok(cmd) = serde_json::from_slice::<Command>(&payload) {
match cmd {
Command::GetStatus => {
let rec = is_recording_state.load(Ordering::SeqCst);
let start_ts = record_start_state.load(Ordering::SeqCst);
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_secs();
let duration = if rec && start_ts > 0 {
now.saturating_sub(start_ts)
} else {
0
};
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),
};
if let Ok(resp) = serde_json::to_vec(&status) {
let len_resp = (resp.len() as u32).to_le_bytes();
let _ = stream.write_all(&len_resp);
let _ = stream.write_all(&resp);
}
},
Command::StopDaemon => {
break;
},

View file

@ -6,9 +6,19 @@ pub enum Command {
StartRecording,
StopRecording,
ToggleRecording,
ToggleAudio,
ReloadConfig,
StopDaemon,
ShowOverlay,
GetStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, Default)]
pub struct DaemonStatus {
pub is_recording: bool,
pub recording_duration_sec: u64,
pub is_replay_active: bool,
pub audio_muted: bool,
}
pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
@ -24,3 +34,24 @@ pub fn send_command(cmd: Command) -> Result<(), Box<dyn std::error::Error + Send
stream.write_all(&payload)?;
Ok(())
}
pub fn query_status() -> Result<DaemonStatus, Box<dyn std::error::Error + Send + Sync>> {
use std::env;
use std::os::unix::net::UnixStream;
use std::io::{Read, Write};
let socket_path = format!("{}/vrec.sock", env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".to_string()));
let mut stream = UnixStream::connect(&socket_path)?;
let payload = serde_json::to_vec(&Command::GetStatus)?;
let len_buf = (payload.len() as u32).to_le_bytes();
stream.write_all(&len_buf)?;
stream.write_all(&payload)?;
let mut resp_len_buf = [0u8; 4];
stream.read_exact(&mut resp_len_buf)?;
let resp_len = u32::from_le_bytes(resp_len_buf) as usize;
let mut resp_payload = vec![0u8; resp_len];
stream.read_exact(&mut resp_payload)?;
let status: DaemonStatus = serde_json::from_slice(&resp_payload)?;
Ok(status)
}

View file

@ -1,9 +1,14 @@
use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Label, CssProvider, StyleContext};
use gtk_layer_shell::{LayerShell, Layer};
use gtk::{
Application, ApplicationWindow, Box, Button, ComboBoxText, CssProvider, Entry, Label,
Orientation, Stack, StackTransitionType, StyleContext, Switch,
};
use gtk_layer_shell::{Layer, LayerShell};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
use crate::config::VrecConfig;
use crate::settings_ui;
use crate::ipc::{self, Command};
pub fn show_notification_overlay() {
if std::env::var("WAYLAND_DISPLAY").is_err() && std::env::var("DISPLAY").is_err() {
@ -20,44 +25,48 @@ pub fn show_notification_overlay() {
app.connect_activate(|app| {
let window = ApplicationWindow::builder()
.application(app)
.default_width(300)
.default_height(80)
.default_width(320)
.default_height(70)
.build();
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_namespace("vrec-overlay");
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 40);
window.set_namespace("vrec-notification");
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 30);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
let css_provider = CssProvider::new();
let css = r#"
window {
background-color: rgba(30, 30, 30, 0.9);
border-radius: 12px;
background-color: rgba(18, 18, 22, 0.94);
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.15);
box-shadow: 0px 8px 24px rgba(0, 0, 0, 0.6);
}
label {
color: #ffffff;
font-weight: bold;
font-size: 20px;
padding: 15px;
box-shadow: 0px 4px 15px rgba(0, 0, 0, 0.5);
font-size: 17px;
padding: 12px 24px;
}
"#;
css_provider.load_from_data(css.as_bytes()).unwrap();
StyleContext::add_provider_for_screen(
&gdk::Screen::default().unwrap(),
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
let _ = css_provider.load_from_data(css.as_bytes());
if let Some(screen) = gdk::Screen::default() {
StyleContext::add_provider_for_screen(
&screen,
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
let label = Label::new(Some("✅ Replay Saved!"));
window.add(&label);
window.show_all();
let window_clone = window.clone();
glib::timeout_add_local(Duration::from_secs(2), move || {
gtk::glib::timeout_add_local(Duration::from_secs(2), move || {
window_clone.close();
glib::ControlFlow::Break
gtk::glib::ControlFlow::Break
});
});
@ -75,86 +84,310 @@ pub fn show_menu_overlay() {
}
let app = Application::builder()
.application_id("com.vrec.menu")
.application_id("com.vrec.hud")
.build();
app.connect_activate(|app| {
let config = VrecConfig::load();
let window = ApplicationWindow::builder()
.application(app)
.default_width(700)
.default_height(200)
.default_width(820)
.default_height(220)
.build();
window.init_layer_shell();
window.set_layer(Layer::Overlay);
window.set_namespace("vrec-menu");
window.set_namespace("vrec-hud");
window.set_keyboard_interactivity(true);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 100);
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 70);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
let main_box = gtk::Box::new(gtk::Orientation::Horizontal, 20);
main_box.set_margin_start(20);
main_box.set_margin_end(20);
main_box.set_margin_top(20);
main_box.set_margin_bottom(20);
main_box.set_halign(gtk::Align::Center);
let replay_box = gtk::Box::new(gtk::Orientation::Vertical, 10);
let replay_label = if config.replay_enabled { "Instant Replay\n🟢 ON" } else { "Instant Replay\n⚪ OFF" };
let replay_btn = gtk::Button::with_label(replay_label);
replay_btn.set_size_request(200, 100);
let replay_settings_btn = gtk::Button::with_label("⚙️ Replay Settings");
let cfg_clone1 = config.clone();
replay_settings_btn.connect_clicked(move |_| {
settings_ui::open_replay_settings(&cfg_clone1);
// 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);
// ==========================================
// PAGE 1: HUD Quick Actions (Xbox / GPU Screen Recorder layout)
// ==========================================
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);
// Header
let header_box = Box::new(Orientation::Horizontal, 10);
let title_label = Label::new(Some("vrec"));
title_label.style_context().add_class("hud-title");
let badge_label = Label::new(Some("GPU ZERO-COPY"));
badge_label.style_context().add_class("badge-tag");
let spacer = Box::new(Orientation::Horizontal, 0);
let close_btn = Button::with_label("✕");
close_btn.style_context().add_class("close-btn");
header_box.pack_start(&title_label, false, false, 0);
header_box.pack_start(&badge_label, false, false, 6);
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);
cards_box.set_halign(gtk::Align::Center);
// Card 1: Instant 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\n🟢 ACTIVE"
} else {
"Instant Replay\n⚪ OFF"
});
replay_box.pack_start(&replay_btn, true, true, 0);
replay_box.pack_start(&replay_settings_btn, false, false, 0);
replay_toggle_btn.set_size_request(170, 75);
replay_toggle_btn.style_context().add_class("action-btn");
let record_box = gtk::Box::new(gtk::Orientation::Vertical, 10);
let record_label = if config.record_enabled { "Recording\n🔴 ON" } else { "Recording\n⚪ OFF" };
let record_btn = gtk::Button::with_label(record_label);
record_btn.set_size_request(200, 100);
let record_settings_btn = gtk::Button::with_label("⚙️ Record Settings");
let cfg_clone2 = config.clone();
record_settings_btn.connect_clicked(move |_| {
settings_ui::open_record_settings(&cfg_clone2);
});
record_box.pack_start(&record_btn, true, true, 0);
record_box.pack_start(&record_settings_btn, false, false, 0);
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_save_btn, false, false, 0);
let settings_box = gtk::Box::new(gtk::Orientation::Vertical, 10);
let gen_settings_btn = gtk::Button::with_label("General Settings");
gen_settings_btn.set_size_request(200, 50);
let cfg_clone3 = config.clone();
gen_settings_btn.connect_clicked(move |_| {
settings_ui::open_general_settings(&cfg_clone3);
});
// Card 2: Normal Recording
let record_card = Box::new(Orientation::Vertical, 8);
record_card.style_context().add_class("card-box");
let record_toggle_btn = Button::with_label("🔴 Record\nClick to Start");
record_toggle_btn.set_size_request(170, 75);
record_toggle_btn.style_context().add_class("action-btn");
let close_btn = gtk::Button::with_label("❌ Close Menu");
close_btn.set_size_request(200, 50);
let spacer = gtk::Box::new(gtk::Orientation::Vertical, 0);
settings_box.pack_start(&gen_settings_btn, false, false, 0);
settings_box.pack_start(&spacer, true, true, 0);
settings_box.pack_start(&close_btn, false, false, 0);
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);
main_box.pack_start(&replay_box, true, true, 0);
main_box.pack_start(&record_box, true, true, 0);
main_box.pack_start(&settings_box, true, true, 0);
// Card 3: Microphone & Audio
let audio_card = Box::new(Orientation::Vertical, 8);
audio_card.style_context().add_class("card-box");
let mic_toggle_btn = Button::with_label("🎙️ Mic / Audio\n🟢 LIVE");
mic_toggle_btn.set_size_request(170, 75);
mic_toggle_btn.style_context().add_class("action-btn");
window.add(&main_box);
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 window_clone = window.clone();
// 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\nPreferences");
settings_open_btn.set_size_request(170, 75);
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);
cards_box.pack_start(&replay_card, true, true, 0);
cards_box.pack_start(&record_card, true, true, 0);
cards_box.pack_start(&audio_card, true, true, 0);
cards_box.pack_start(&settings_card, true, true, 0);
hud_page.pack_start(&header_box, false, false, 0);
hud_page.pack_start(&cards_box, true, true, 0);
// ==========================================
// PAGE 2: Inline Settings Panel (Zero separate windows!)
// ==========================================
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_header = Box::new(Orientation::Horizontal, 12);
let back_btn = Button::with_label("⬅️ Back");
back_btn.style_context().add_class("sub-btn");
let settings_title = Label::new(Some("Recorder Settings"));
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);
// Replay Duration
let dur_lbl = Label::new(Some("Replay Duration:"));
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()));
// Video Bitrate
let bit_lbl = Label::new(Some("Recording Quality:"));
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()));
// 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);
// 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);
settings_grid.attach(&dur_lbl, 0, 0, 1, 1);
settings_grid.attach(&dur_combo, 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(&auto_lbl, 0, 2, 1, 1);
settings_grid.attach(&auto_switch, 1, 2, 1, 1);
settings_grid.attach(&save_hk_lbl, 0, 3, 1, 1);
settings_grid.attach(&save_hk_entry, 1, 3, 1, 1);
settings_grid.attach(&menu_hk_lbl, 0, 4, 1, 1);
settings_grid.attach(&menu_hk_entry, 1, 4, 1, 1);
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);
settings_page.pack_start(&settings_grid, true, true, 4);
settings_page.pack_start(&save_settings_btn, false, false, 4);
stack.add_named(&hud_page, "hud");
stack.add_named(&settings_page, "settings");
window.add(&stack);
// CSS Styling (Sleek Frosted Glass HUD)
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;
}
.hud-title {
font-size: 20px;
font-weight: 900;
color: #ffffff;
letter-spacing: 1px;
}
.badge-tag {
background-color: rgba(56, 189, 248, 0.2);
color: #38bdf8;
font-size: 10px;
font-weight: bold;
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;
}
.card-box:hover {
background-color: rgba(255, 255, 255, 0.07);
border-color: rgba(255, 255, 255, 0.18);
}
.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;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
font-size: 12px;
font-weight: 600;
padding: 6px 12px;
}
.sub-btn:hover {
background-color: rgba(255, 255, 255, 0.16);
color: #ffffff;
}
.apply-btn {
background-color: #2563eb;
color: #ffffff;
border: none;
border-radius: 10px;
font-size: 14px;
font-weight: bold;
padding: 10px 20px;
}
.apply-btn:hover {
background-color: #1d4ed8;
}
.close-btn {
background-color: transparent;
color: #94a3b8;
border: none;
font-size: 18px;
font-weight: bold;
padding: 4px 8px;
}
.close-btn:hover {
color: #f43f5e;
}
.sub-label {
color: #64748b;
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);
border-radius: 8px;
padding: 6px 10px;
}
"#;
let _ = css_provider.load_from_data(css.as_bytes());
if let Some(screen) = gdk::Screen::default() {
StyleContext::add_provider_for_screen(
&screen,
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
// Window Close Triggers
let window_close = window.clone();
close_btn.connect_clicked(move |_| {
window_clone.close();
window_close.close();
});
let window_esc = window.clone();
@ -167,61 +400,106 @@ pub fn show_menu_overlay() {
}
});
let replay_btn_clone = replay_btn.clone();
replay_btn.connect_clicked(move |_| {
// Stack Navigation
let stack_to_settings = stack.clone();
settings_open_btn.connect_clicked(move |_| {
stack_to_settings.set_visible_child_name("settings");
});
let stack_to_hud = stack.clone();
back_btn.connect_clicked(move |_| {
stack_to_hud.set_visible_child_name("hud");
});
// Save Settings Action
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;
}
cfg.autostart = auto_switch.is_active();
cfg.save_hotkey = save_hk_entry.text().to_string();
cfg.menu_hotkey = menu_hk_entry.text().to_string();
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
stack_after_save.set_visible_child_name("hud");
});
// Action Handlers
let replay_lbl_btn = replay_toggle_btn.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_btn_clone.set_label("Instant Replay\n🟢 ON");
replay_lbl_btn.set_label("Instant Replay\n🟢 ACTIVE");
} else {
replay_btn_clone.set_label("Instant Replay\n⚪ OFF");
replay_lbl_btn.set_label("Instant Replay\n⚪ OFF");
}
});
let record_btn_clone = record_btn.clone();
record_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.record_enabled = !cfg.record_enabled;
let _ = cfg.save();
VrecConfig::notify_daemon_reload();
if cfg.record_enabled {
record_btn_clone.set_label("Recording\n🔴 ON");
let _ = crate::ipc::send_command(crate::ipc::Command::StartRecording);
} else {
record_btn_clone.set_label("Recording\n⚪ OFF");
let _ = crate::ipc::send_command(crate::ipc::Command::StopRecording);
}
replay_save_btn.connect_clicked(|_| {
let _ = ipc::send_command(Command::SaveReplay);
show_notification_overlay();
});
record_toggle_btn.connect_clicked(|_| {
let _ = ipc::send_command(Command::ToggleRecording);
});
let css_provider = CssProvider::new();
let css = r#"
window {
background-color: rgba(25, 25, 25, 0.95);
border-radius: 16px;
color: #ffffff;
mic_toggle_btn.connect_clicked(|_| {
let _ = ipc::send_command(Command::ToggleAudio);
});
// Periodic Live Status Query from Daemon (Syncs live recording state & timer!)
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 is_running = Arc::new(AtomicBool::new(true));
let is_running_clone = Arc::clone(&is_running);
gtk::glib::timeout_add_local(Duration::from_millis(500), move || {
if !is_running_clone.load(Ordering::Relaxed) {
return gtk::glib::ControlFlow::Break;
}
button {
background-color: rgba(50, 50, 50, 0.8);
color: white;
border-radius: 12px;
padding: 10px;
font-weight: bold;
font-size: 16px;
border: 1px solid rgba(100, 100, 100, 0.5);
if let Ok(status) = ipc::query_status() {
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.style_context().add_class("recording-active");
} else {
rec_btn_sync.set_label("🔴 Record\nClick to Start");
rec_btn_sync.style_context().remove_class("recording-active");
}
if status.audio_muted {
mic_btn_sync.set_label("🔇 Mic / Audio\n⚪ MUTED");
} else {
mic_btn_sync.set_label("🎙️ Mic / Audio\n🟢 LIVE");
}
if status.is_replay_active {
replay_btn_sync.set_label("Instant Replay\n🟢 ACTIVE");
} else {
replay_btn_sync.set_label("Instant Replay\n⚪ OFF");
}
}
button:hover {
background-color: rgba(80, 80, 80, 0.9);
}
"#;
css_provider.load_from_data(css.as_bytes()).unwrap();
StyleContext::add_provider_for_screen(
&gdk::Screen::default().unwrap(),
&css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
gtk::glib::ControlFlow::Continue
});
window.connect_destroy(move |_| {
is_running.store(false, Ordering::Relaxed);
});
window.show_all();
});