feat: Connect GUI overlay to JSON config and implement interactive GTK settings dialogs

This commit is contained in:
katR 2026-09-02 07:17:04 +05:30
parent 85b90b73f8
commit 826166d925
6 changed files with 334 additions and 11 deletions

63
Cargo.lock generated
View file

@ -35,7 +35,7 @@ checksum = "fb8421aaa9644a5faf26735f258b669b15f063313ef8f8e2bdb28912a1a6f111"
dependencies = [
"enumflags2",
"futures-util",
"getrandom",
"getrandom 0.4.3",
"serde",
"serde_repr",
"tokio",
@ -253,6 +253,27 @@ version = "0.8.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
[[package]]
name = "dirs"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
dependencies = [
"dirs-sys",
]
[[package]]
name = "dirs-sys"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.61.2",
]
[[package]]
name = "dispatch2"
version = "0.3.1"
@ -522,6 +543,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "getrandom"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
dependencies = [
"cfg-if",
"libc",
"wasi",
]
[[package]]
name = "getrandom"
version = "0.4.3"
@ -886,6 +918,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "libredox"
version = "0.1.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed"
dependencies = [
"libc",
]
[[package]]
name = "libspa"
version = "0.10.1"
@ -1097,6 +1138,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "option-ext"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
[[package]]
name = "ordered-stream"
version = "0.2.0"
@ -1276,6 +1323,17 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "redox_users"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
dependencies = [
"getrandom 0.2.17",
"libredox",
"thiserror 2.0.20",
]
[[package]]
name = "regex"
version = "1.13.1"
@ -1562,7 +1620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
dependencies = [
"fastrand",
"getrandom",
"getrandom 0.4.3",
"once_cell",
"rustix",
"windows-sys 0.61.2",
@ -1827,6 +1885,7 @@ version = "0.1.0"
dependencies = [
"ashpd",
"crossbeam-channel",
"dirs",
"ffmpeg-next",
"ffmpeg-sys-next",
"gdk",

View file

@ -22,3 +22,4 @@ serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread"] }
xcb = { version = "1.7.1", features = ["shm", "xfixes"] }
dirs = "6.0.0"

69
src/config.rs Normal file
View file

@ -0,0 +1,69 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VrecConfig {
pub replay_enabled: bool,
pub replay_duration_sec: u32,
pub replay_bitrate_kbps: u32,
pub record_enabled: bool,
pub record_bitrate_kbps: u32,
pub autostart: bool,
pub language: String,
pub ui_color_theme: String,
pub save_hotkey: String,
pub menu_hotkey: String,
}
impl Default for VrecConfig {
fn default() -> Self {
Self {
replay_enabled: true,
replay_duration_sec: 60,
replay_bitrate_kbps: 10000,
record_enabled: false,
record_bitrate_kbps: 15000,
autostart: false,
language: "en".to_string(),
ui_color_theme: "dark".to_string(),
save_hotkey: "Ctrl+Shift+R".to_string(),
menu_hotkey: "Alt+Z".to_string(),
}
}
}
impl VrecConfig {
pub fn config_path() -> PathBuf {
let mut path = dirs::config_dir().unwrap_or_else(|| PathBuf::from("."));
path.push("vrec");
path.push("config.json");
path
}
pub fn load() -> Self {
let path = Self::config_path();
if path.exists() {
if let Ok(data) = fs::read_to_string(path) {
if let Ok(config) = serde_json::from_str(&data) {
return config;
}
}
}
Self::default()
}
pub fn save(&self) -> Result<(), std::io::Error> {
let path = Self::config_path();
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let data = serde_json::to_string_pretty(self)?;
fs::write(path, data)?;
Ok(())
}
}

View file

@ -5,3 +5,5 @@ pub mod muxer;
pub mod hotkey;
pub mod ipc;
pub mod overlay;
pub mod config;
pub mod settings_ui;

View file

@ -2,6 +2,8 @@ use gtk::prelude::*;
use gtk::{Application, ApplicationWindow, Label, CssProvider, StyleContext};
use gtk_layer_shell::{LayerShell, Layer};
use std::time::Duration;
use crate::config::VrecConfig;
use crate::settings_ui;
pub fn show_notification_overlay() {
let app = Application::builder()
@ -21,7 +23,6 @@ pub fn show_notification_overlay() {
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 40);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
// Make it transparent and borderless
let css_provider = CssProvider::new();
let css = r#"
window {
@ -46,7 +47,6 @@ pub fn show_notification_overlay() {
window.show_all();
// Close after 2 seconds
let window_clone = window.clone();
glib::timeout_add_local(Duration::from_secs(2), move || {
window_clone.close();
@ -63,6 +63,8 @@ pub fn show_menu_overlay() {
.build();
app.connect_activate(|app| {
let config = VrecConfig::load();
let window = ApplicationWindow::builder()
.application(app)
.default_width(700)
@ -76,7 +78,6 @@ pub fn show_menu_overlay() {
window.set_layer_shell_margin(gtk_layer_shell::Edge::Top, 100);
window.set_anchor(gtk_layer_shell::Edge::Top, true);
// Main layout
let main_box = gtk::Box::new(gtk::Orientation::Horizontal, 20);
main_box.set_margin_start(20);
main_box.set_margin_end(20);
@ -84,26 +85,43 @@ pub fn show_menu_overlay() {
main_box.set_margin_bottom(20);
main_box.set_halign(gtk::Align::Center);
// 1. Replay Box
let replay_box = gtk::Box::new(gtk::Orientation::Vertical, 10);
let replay_btn = gtk::Button::with_label("Instant Replay\n🟢 ON");
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);
});
replay_box.pack_start(&replay_btn, true, true, 0);
replay_box.pack_start(&replay_settings_btn, false, false, 0);
// 2. Record Box
let record_box = gtk::Box::new(gtk::Orientation::Vertical, 10);
let record_btn = gtk::Button::with_label("Recording\n⚪ OFF");
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);
// 3. Settings Box
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);
});
let close_btn = gtk::Button::with_label("❌ Close Menu");
close_btn.set_size_request(200, 50);
@ -123,7 +141,34 @@ pub fn show_menu_overlay() {
window_clone.close();
});
// Add CSS styling
// Toggle buttons logic (just UI for now, backend IPC will be needed to actually toggle daemon state)
let btn_cfg_1 = config.clone();
let replay_btn_clone = replay_btn.clone();
replay_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.replay_enabled = !cfg.replay_enabled;
cfg.save().unwrap();
if cfg.replay_enabled {
replay_btn_clone.set_label("Instant Replay\n🟢 ON");
} else {
replay_btn_clone.set_label("Instant Replay\n⚪ OFF");
}
});
let btn_cfg_2 = config.clone();
let record_btn_clone = record_btn.clone();
record_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.record_enabled = !cfg.record_enabled;
cfg.save().unwrap();
if cfg.record_enabled {
record_btn_clone.set_label("Recording\n🔴 ON");
} else {
record_btn_clone.set_label("Recording\n⚪ OFF");
}
});
let css_provider = CssProvider::new();
let css = r#"
window {

147
src/settings_ui.rs Normal file
View file

@ -0,0 +1,147 @@
use gtk::prelude::*;
use gtk::{Window, WindowType, Box, Orientation, Label, Entry, Button, Switch, ComboBoxText};
use crate::config::VrecConfig;
pub fn open_replay_settings(config_in: &VrecConfig) {
let window = Window::new(WindowType::Toplevel);
window.set_title("Replay Settings");
window.set_default_size(300, 200);
let vbox = Box::new(Orientation::Vertical, 10);
vbox.set_margin(15);
let duration_label = Label::new(Some("Replay Duration (seconds):"));
let duration_entry = Entry::new();
duration_entry.set_text(&config_in.replay_duration_sec.to_string());
let bitrate_label = Label::new(Some("Bitrate (kbps):"));
let bitrate_entry = Entry::new();
bitrate_entry.set_text(&config_in.replay_bitrate_kbps.to_string());
let save_btn = Button::with_label("Save");
vbox.pack_start(&duration_label, false, false, 0);
vbox.pack_start(&duration_entry, false, false, 0);
vbox.pack_start(&bitrate_label, false, false, 0);
vbox.pack_start(&bitrate_entry, false, false, 0);
vbox.pack_start(&save_btn, false, false, 0);
window.add(&vbox);
let window_clone = window.clone();
save_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
if let Ok(val) = duration_entry.text().parse() {
cfg.replay_duration_sec = val;
}
if let Ok(val) = bitrate_entry.text().parse() {
cfg.replay_bitrate_kbps = val;
}
let _ = cfg.save();
window_clone.close();
});
window.show_all();
}
pub fn open_record_settings(config_in: &VrecConfig) {
let window = Window::new(WindowType::Toplevel);
window.set_title("Record Settings");
window.set_default_size(300, 150);
let vbox = Box::new(Orientation::Vertical, 10);
vbox.set_margin(15);
let bitrate_label = Label::new(Some("Recording Bitrate (kbps):"));
let bitrate_entry = Entry::new();
bitrate_entry.set_text(&config_in.record_bitrate_kbps.to_string());
let save_btn = Button::with_label("Save");
vbox.pack_start(&bitrate_label, false, false, 0);
vbox.pack_start(&bitrate_entry, false, false, 0);
vbox.pack_start(&save_btn, false, false, 0);
window.add(&vbox);
let window_clone = window.clone();
save_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
if let Ok(val) = bitrate_entry.text().parse() {
cfg.record_bitrate_kbps = val;
}
let _ = cfg.save();
window_clone.close();
});
window.show_all();
}
pub fn open_general_settings(config_in: &VrecConfig) {
let window = Window::new(WindowType::Toplevel);
window.set_title("General Settings");
window.set_default_size(350, 300);
let vbox = Box::new(Orientation::Vertical, 10);
vbox.set_margin(15);
// Autostart
let hbox_auto = Box::new(Orientation::Horizontal, 10);
let auto_label = Label::new(Some("Autostart with system:"));
let auto_switch = Switch::new();
auto_switch.set_active(config_in.autostart);
hbox_auto.pack_start(&auto_label, false, false, 0);
hbox_auto.pack_start(&auto_switch, false, false, 0);
// Theme
let theme_label = Label::new(Some("UI Theme:"));
let theme_combo = ComboBoxText::new();
theme_combo.append_text("dark");
theme_combo.append_text("light");
theme_combo.set_active(Some(if config_in.ui_color_theme == "dark" { 0 } else { 1 }));
// Language
let lang_label = Label::new(Some("Language Code (e.g. en, fr):"));
let lang_entry = Entry::new();
lang_entry.set_text(&config_in.language);
// Hotkeys
let menu_hk_label = Label::new(Some("Menu Hotkey:"));
let menu_hk_entry = Entry::new();
menu_hk_entry.set_text(&config_in.menu_hotkey);
let save_hk_label = Label::new(Some("Save Replay Hotkey:"));
let save_hk_entry = Entry::new();
save_hk_entry.set_text(&config_in.save_hotkey);
let save_btn = Button::with_label("Save General Settings");
vbox.pack_start(&hbox_auto, false, false, 0);
vbox.pack_start(&theme_label, false, false, 0);
vbox.pack_start(&theme_combo, false, false, 0);
vbox.pack_start(&lang_label, false, false, 0);
vbox.pack_start(&lang_entry, false, false, 0);
vbox.pack_start(&menu_hk_label, false, false, 0);
vbox.pack_start(&menu_hk_entry, false, false, 0);
vbox.pack_start(&save_hk_label, false, false, 0);
vbox.pack_start(&save_hk_entry, false, false, 0);
vbox.pack_start(&save_btn, false, false, 0);
window.add(&vbox);
let window_clone = window.clone();
save_btn.connect_clicked(move |_| {
let mut cfg = VrecConfig::load();
cfg.autostart = auto_switch.is_active();
if let Some(txt) = theme_combo.active_text() {
cfg.ui_color_theme = txt.to_string();
}
cfg.language = lang_entry.text().to_string();
cfg.menu_hotkey = menu_hk_entry.text().to_string();
cfg.save_hotkey = save_hk_entry.text().to_string();
let _ = cfg.save();
window_clone.close();
});
window.show_all();
}