Rebuild UI with egui, fix VAAPI AMDGPU encoding, and add dynamic Hyprland keybinds

This commit is contained in:
katR 2026-09-04 07:51:45 +05:30
parent 8291b267aa
commit e3624246f8
9 changed files with 3619 additions and 81 deletions

3067
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,8 @@ tokio = { version = "1.53.1", features = ["rt", "macros", "rt-multi-thread", "si
dirs = "6.0.0"
cpal = "0.18.2"
rubato = "5.0.0"
eframe = "0.31"
egui = "0.31"
[target.'cfg(target_os = "linux")'.dependencies]
ashpd = { version = "0.13.13", features = ["screencast"] }

View file

@ -105,6 +105,10 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
&initial_config.audio_mode,
).ok();
// Automatically register dynamic keybindings on Hyprland if active (no config edits needed)
vrec::hyprland_binds::register_hyprland_binds(&initial_config);
vrec::hyprland_binds::spawn_hyprland_reload_watcher();
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));
@ -197,6 +201,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
ring = HeapRb::<Packet>::new(new_capacity);
println!("Replay buffer resized to {} packets.", new_capacity);
}
vrec::hyprland_binds::register_hyprland_binds(&config);
},
Command::SaveReplay => {
if config.replay_enabled {
@ -382,6 +387,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
},
Command::StopDaemon => {
println!("StopDaemon requested: Finalizing active recordings...");
vrec::hyprland_binds::unregister_hyprland_binds(&vrec::config::VrecConfig::load());
let _ = cmd_tx.send(Command::StopRecording);
std::thread::sleep(std::time::Duration::from_millis(350));
break;

View file

@ -1,5 +1,5 @@
use vrec::ipc::{Command, send_command, query_status};
use vrec::overlay::{show_menu_overlay, show_notification};
use vrec::overlay::show_notification;
use std::env;
use global_hotkey::{GlobalHotKeyManager, hotkey::{HotKey, Modifiers, Code}, GlobalHotKeyEvent};
@ -98,7 +98,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
}
"--menu" => {
ensure_daemon_running();
show_menu_overlay();
vrec::overlay_egui::run_egui_overlay();
return Ok(());
}
"--record" | "--toggle" => {
@ -148,6 +148,8 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
println!(" You can bind `vrec-ui --menu` or `vrec-ui --save` in your system shortcut settings.");
} else {
println!("wlroots/Hyprland compositor detected.");
vrec::hyprland_binds::register_hyprland_binds(&vrec::config::VrecConfig::load());
vrec::hyprland_binds::spawn_hyprland_reload_watcher();
}
} else {
println!("X11 session detected.");
@ -169,7 +171,7 @@ fn main() -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
if let Ok(event) = receiver.try_recv() {
if event.id == hotkey_menu.id() {
ensure_daemon_running();
show_menu_overlay();
vrec::overlay_egui::run_egui_overlay();
} else if event.id == hotkey_save.id() {
let _ = send_with_notification(Command::SaveReplay, "Replay saved");
} else if event.id == hotkey_record.id() {

View file

@ -28,7 +28,7 @@ impl WaylandCapture {
.set_multiple(false)
.set_cursor_mode(CursorMode::Embedded)
.set_sources(SourceType::Monitor | SourceType::Monitor)
.set_persist_mode(PersistMode::Application);
.set_persist_mode(PersistMode::DoNot);
proxy.select_sources(&session, select_opts).await.map_err(|e| {
format!("Failed to select ScreenCast sources: {:?}", e)

View file

@ -232,7 +232,86 @@ impl VaapiEncoder {
av_packet_free(&mut pkt);
}
} else {
eprintln!("Failed to map DRM prime to VAAPI: {}", map_ret);
let mmap_size = (*stride as usize) * (*dma_height as usize);
let ptr = libc::mmap(
ptr::null_mut(),
mmap_size,
libc::PROT_READ,
libc::MAP_SHARED,
*fd,
*offset as libc::off_t,
);
if ptr != libc::MAP_FAILED {
let mut bgra_frame = av_frame_alloc();
(*bgra_frame).format = AVPixelFormat::AV_PIX_FMT_BGRA as i32;
(*bgra_frame).width = *dma_width as i32;
(*bgra_frame).height = *dma_height as i32;
av_frame_get_buffer(bgra_frame, 32);
let bgra_stride = (*bgra_frame).linesize[0] as usize;
for y in 0..(*dma_height as usize) {
let src_row = std::slice::from_raw_parts(
(ptr as *const u8).add(y * (*stride as usize)),
(*dma_width as usize) * 4
);
let dst_row = std::slice::from_raw_parts_mut(
(*bgra_frame).data[0].add(y * bgra_stride),
(*dma_width as usize) * 4
);
dst_row.copy_from_slice(src_row);
}
libc::munmap(ptr, mmap_size);
let mut nv12_frame = av_frame_alloc();
(*nv12_frame).format = AVPixelFormat::AV_PIX_FMT_NV12 as i32;
(*nv12_frame).width = *dma_width as i32;
(*nv12_frame).height = *dma_height as i32;
av_frame_get_buffer(nv12_frame, 32);
if self.sws_ctx.is_null() {
self.sws_ctx = sws_getContext(
*dma_width as i32, *dma_height as i32, AVPixelFormat::AV_PIX_FMT_BGRA,
*dma_width as i32, *dma_height as i32, AVPixelFormat::AV_PIX_FMT_NV12,
2, ptr::null_mut(), ptr::null_mut(), ptr::null_mut()
);
}
sws_scale(
self.sws_ctx,
(*bgra_frame).data.as_ptr() as *const *const u8,
(*bgra_frame).linesize.as_ptr(),
0,
*dma_height as i32,
(*nv12_frame).data.as_ptr(),
(*nv12_frame).linesize.as_ptr()
);
let mut fresh_hw_frame = av_frame_alloc();
(*fresh_hw_frame).format = AVPixelFormat::AV_PIX_FMT_VAAPI as i32;
let get_buf_ret = av_hwframe_get_buffer(self.hw_frames_ctx, fresh_hw_frame, 0);
let transfer_ret = av_hwframe_transfer_data(fresh_hw_frame, nv12_frame, 0);
(*fresh_hw_frame).pts = self.next_pts;
self.next_pts += 1;
let send_ret = avcodec_send_frame(self.codec_ctx, fresh_hw_frame);
if send_ret >= 0 {
let mut pkt = av_packet_alloc();
while avcodec_receive_packet(self.codec_ctx, pkt) >= 0 {
let new_pkt = av_packet_alloc();
av_packet_move_ref(new_pkt, pkt);
packets.push(crate::ring::Packet::new(new_pkt));
}
av_packet_free(&mut pkt);
} else {
eprintln!("get_buf: {}, transfer: {}, send_ret: {}", get_buf_ret, transfer_ret, send_ret);
}
av_frame_free(&mut fresh_hw_frame);
av_frame_free(&mut nv12_frame);
av_frame_free(&mut bgra_frame);
} else {
eprintln!("mmap failed on dma-buf fd {}: {}", *fd, std::io::Error::last_os_error());
}
}
}

181
src/hyprland_binds.rs Normal file
View file

@ -0,0 +1,181 @@
use std::process::Command;
use std::env;
use std::io::{BufRead, BufReader};
use std::os::unix::net::UnixStream;
use std::path::PathBuf;
use crate::config::VrecConfig;
pub fn is_hyprland() -> bool {
env::var("HYPRLAND_INSTANCE_SIGNATURE").is_ok()
|| env::var("XDG_CURRENT_DESKTOP")
.map(|d| d.to_lowercase().contains("hyprland"))
.unwrap_or(false)
}
/// Convert standard hotkey format (e.g. "Ctrl+Shift+R") to Hyprland bind format ("CONTROL_SHIFT, R")
pub fn hotkey_to_hyprland(hotkey: &str) -> Option<(String, String)> {
let parts: Vec<&str> = hotkey.split('+').map(|s| s.trim()).collect();
if parts.is_empty() {
return None;
}
let mut mods = Vec::new();
let mut key = "";
for (i, &part) in parts.iter().enumerate() {
if i == parts.len() - 1 {
key = part;
} else {
let m = match part.to_lowercase().as_str() {
"ctrl" | "control" => "CONTROL",
"shift" => "SHIFT",
"alt" => "ALT",
"super" | "win" | "meta" => "SUPER",
_ => continue,
};
mods.push(m);
}
}
if key.is_empty() {
return None;
}
let mod_str = if mods.is_empty() {
"".to_string()
} else {
mods.join("_")
};
Some((mod_str, key.to_string()))
}
/// Dynamically inject binds into running Hyprland without touching hyprland.conf
pub fn register_hyprland_binds(config: &VrecConfig) {
if !is_hyprland() {
return;
}
let binds = [
(&config.menu_hotkey, "exec, vrec-ui --menu"),
(&config.save_hotkey, "exec, vrec-ui --save"),
(&config.record_hotkey, "exec, vrec-ui --record"),
];
for (hotkey, action) in binds {
if let Some((mods, key)) = hotkey_to_hyprland(hotkey) {
let bind_arg = if mods.is_empty() {
format!("{}, {}", key, action)
} else {
format!("{}, {}, {}", mods, key, action)
};
let res = Command::new("hyprctl")
.args(["keyword", "bind", &bind_arg])
.output();
if let Ok(out) = res {
if !out.status.success() {
eprintln!("hyprctl keyword bind note: {:?}", String::from_utf8_lossy(&out.stderr));
} else {
println!("Hyprland dynamic bind active: {}", bind_arg);
}
}
}
}
}
/// Dynamically remove binds from running Hyprland
pub fn unregister_hyprland_binds(config: &VrecConfig) {
if !is_hyprland() {
return;
}
let hotkeys = [
&config.menu_hotkey,
&config.save_hotkey,
&config.record_hotkey,
];
for hotkey in hotkeys {
if let Some((mods, key)) = hotkey_to_hyprland(hotkey) {
let unbind_arg = if mods.is_empty() {
key
} else {
format!("{}, {}", mods, key)
};
let _ = Command::new("hyprctl")
.args(["keyword", "unbind", &unbind_arg])
.output();
}
}
}
/// Find Hyprland socket2 for live event listening
fn get_hyprland_socket2() -> Option<PathBuf> {
let sig = env::var("HYPRLAND_INSTANCE_SIGNATURE").ok()?;
let xdg_runtime = env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/run/user/1000".to_string());
let path = PathBuf::from(format!("{}/hypr/{}/.socket2.sock", xdg_runtime, sig));
if path.exists() {
return Some(path);
}
let fallback = PathBuf::from(format!("/tmp/hypr/{}/.socket2.sock", sig));
if fallback.exists() {
return Some(fallback);
}
None
}
/// Background watcher for Hyprland config reloads so dynamic binds are never lost
pub fn spawn_hyprland_reload_watcher() {
if !is_hyprland() {
return;
}
std::thread::spawn(|| {
loop {
if let Some(sock_path) = get_hyprland_socket2() {
if let Ok(stream) = UnixStream::connect(&sock_path) {
let reader = BufReader::new(stream);
for line in reader.lines().map_while(Result::ok) {
if line.starts_with("configreloaded") {
println!("Hyprland config reload detected! Re-registering vrec dynamic binds...");
let config = VrecConfig::load();
register_hyprland_binds(&config);
}
}
}
}
std::thread::sleep(std::time::Duration::from_secs(3));
}
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hotkey_to_hyprland_conversion() {
assert_eq!(
hotkey_to_hyprland("Ctrl+Shift+R"),
Some(("CONTROL_SHIFT".to_string(), "R".to_string()))
);
assert_eq!(
hotkey_to_hyprland("Alt+Z"),
Some(("ALT".to_string(), "Z".to_string()))
);
assert_eq!(
hotkey_to_hyprland("Ctrl+Shift+F9"),
Some(("CONTROL_SHIFT".to_string(), "F9".to_string()))
);
assert_eq!(
hotkey_to_hyprland("F12"),
Some(("".to_string(), "F12".to_string()))
);
assert_eq!(
hotkey_to_hyprland("Super+Alt+S"),
Some(("SUPER_ALT".to_string(), "S".to_string()))
);
}
}

View file

@ -7,3 +7,5 @@ pub mod ipc;
pub mod overlay;
pub mod config;
pub mod settings_ui;
pub mod overlay_egui;
pub mod hyprland_binds;

351
src/overlay_egui.rs Normal file
View file

@ -0,0 +1,351 @@
use eframe::egui;
use egui::{Color32, CornerRadius, FontId, Margin, Stroke, Vec2};
use std::time::{Duration, Instant};
use crate::config::VrecConfig;
use crate::ipc::{self, Command, DaemonStatus};
pub struct VrecOverlayApp {
config: VrecConfig,
status: DaemonStatus,
last_status_fetch: Instant,
show_settings: bool,
replay_sec: u32,
bitrate_mbps: u32,
target_fps: u32,
audio_mode_idx: usize,
status_msg: Option<(String, Instant)>,
}
impl VrecOverlayApp {
pub fn new() -> Self {
let config = VrecConfig::load();
let status = ipc::query_status().unwrap_or_default();
let replay_sec = config.replay_duration_sec;
let bitrate_mbps = (config.record_bitrate_kbps / 1000).max(1);
let target_fps = config.fps;
let audio_mode_idx = match config.audio_mode.as_str() {
"mic" => 1,
"both" => 2,
"muted" => 3,
_ => 0,
};
Self {
config,
status,
last_status_fetch: Instant::now(),
show_settings: false,
replay_sec,
bitrate_mbps,
target_fps,
audio_mode_idx,
status_msg: None,
}
}
fn refresh_status(&mut self) {
if self.last_status_fetch.elapsed() > Duration::from_millis(400) {
if let Ok(s) = ipc::query_status() {
self.status = s;
}
self.last_status_fetch = Instant::now();
}
}
fn set_msg(&mut self, text: &str) {
self.status_msg = Some((text.to_string(), Instant::now()));
}
}
impl eframe::App for VrecOverlayApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.refresh_status();
ctx.request_repaint_after(Duration::from_millis(200));
// Handle Escape key to close
if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
// Custom dark stealth theme matching GPU Screen Recorder
let mut visuals = egui::Visuals::dark();
visuals.panel_fill = Color32::from_rgb(15, 17, 23);
visuals.window_fill = Color32::from_rgb(15, 17, 23);
visuals.window_stroke = Stroke::new(1.0_f32, Color32::from_rgb(45, 52, 64));
visuals.window_corner_radius = CornerRadius::same(14_u8);
ctx.set_visuals(visuals);
egui::CentralPanel::default()
.frame(
egui::Frame::NONE
.fill(Color32::from_rgba_premultiplied(13, 16, 23, 245))
.stroke(Stroke::new(1.5_f32, Color32::from_rgb(38, 44, 58)))
.corner_radius(CornerRadius::same(14_u8))
.inner_margin(Margin::same(16_i8)),
)
.show(ctx, |ui| {
// Top Header Bar
ui.horizontal(|ui| {
ui.label(
egui::RichText::new("VREC")
.font(FontId::proportional(20.0))
.strong()
.color(Color32::from_rgb(88, 166, 255)),
);
let (dot_color, status_text) = if self.status.is_recording {
(Color32::from_rgb(239, 68, 68), "RECORDING")
} else if self.status.is_replay_active {
(Color32::from_rgb(34, 197, 94), "REPLAY READY")
} else {
(Color32::from_rgb(156, 163, 175), "STANDBY")
};
ui.add_space(8.0);
let (dot_rect, _) = ui.allocate_exact_size(Vec2::new(10.0, 10.0), egui::Sense::hover());
ui.painter().circle_filled(dot_rect.center(), 5.0, dot_color);
ui.label(
egui::RichText::new(status_text)
.font(FontId::proportional(12.0))
.color(dot_color)
.strong(),
);
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
if ui
.add(
egui::Button::new(egui::RichText::new("X").strong())
.fill(Color32::from_rgb(30, 34, 45))
.corner_radius(CornerRadius::same(6_u8)),
)
.clicked()
{
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
let settings_text = if self.show_settings { "Close Settings" } else { "Settings" };
let settings_btn = egui::Button::new(egui::RichText::new(settings_text).color(Color32::from_rgb(200, 210, 225)))
.fill(if self.show_settings { Color32::from_rgb(38, 45, 60) } else { Color32::from_rgb(25, 28, 38) })
.corner_radius(CornerRadius::same(6_u8));
if ui.add(settings_btn).clicked() {
self.show_settings = !self.show_settings;
}
});
});
if let Some((msg, time)) = &self.status_msg {
if time.elapsed() < Duration::from_secs(3) {
ui.add_space(4.0);
ui.label(
egui::RichText::new(msg)
.color(Color32::from_rgb(74, 222, 128))
.font(FontId::proportional(12.0)),
);
}
}
ui.add_space(12.0);
// Main HUD Cards Grid (4 Cards)
ui.columns(4, |cols| {
// Card 1: Replay
cols[0].group(|ui| {
ui.set_min_height(110.0);
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new("REPLAY").font(FontId::proportional(13.0)).strong().color(Color32::from_rgb(148, 163, 184)));
let buf_label = format!("{}s Buffer", self.config.replay_duration_sec);
ui.label(egui::RichText::new(buf_label).font(FontId::proportional(11.0)).color(Color32::from_rgb(100, 116, 139)));
ui.add_space(8.0);
if ui.add_sized([120.0, 32.0], egui::Button::new("Save Replay").fill(Color32::from_rgb(16, 185, 129)).corner_radius(CornerRadius::same(8_u8))).clicked() {
let _ = ipc::send_command(Command::SaveReplay);
self.set_msg("Replay save command sent!");
}
});
});
// Card 2: Record
cols[1].group(|ui| {
ui.set_min_height(110.0);
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new("RECORD").font(FontId::proportional(13.0)).strong().color(Color32::from_rgb(148, 163, 184)));
let timer_text = if self.status.is_recording {
let d = self.status.recording_duration_sec;
format!("{:02}:{:02}:{:02}", d / 3600, (d % 3600) / 60, d % 60)
} else {
"Idle".to_string()
};
ui.label(egui::RichText::new(timer_text).font(FontId::proportional(11.0)).color(Color32::from_rgb(100, 116, 139)));
ui.add_space(8.0);
let (rec_label, rec_color) = if self.status.is_recording {
("Stop", Color32::from_rgb(239, 68, 68))
} else {
("Start", Color32::from_rgb(59, 130, 246))
};
if ui.add_sized([120.0, 32.0], egui::Button::new(rec_label).fill(rec_color).corner_radius(CornerRadius::same(8_u8))).clicked() {
let _ = ipc::send_command(Command::ToggleRecording);
self.set_msg("Recording toggled!");
}
});
});
// Card 3: Audio
cols[2].group(|ui| {
ui.set_min_height(110.0);
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new("AUDIO").font(FontId::proportional(13.0)).strong().color(Color32::from_rgb(148, 163, 184)));
let mode_name = match self.status.audio_mode.as_str() {
"mic" => "Microphone",
"both" => "System + Mic",
"muted" => "Muted",
_ => "System Audio",
};
ui.label(egui::RichText::new(mode_name).font(FontId::proportional(11.0)).color(Color32::from_rgb(100, 116, 139)));
ui.add_space(8.0);
if ui.add_sized([120.0, 32.0], egui::Button::new("Cycle Mode").fill(Color32::from_rgb(30, 41, 59)).corner_radius(CornerRadius::same(8_u8))).clicked() {
let _ = ipc::send_command(Command::CycleAudioMode);
self.refresh_status();
self.set_msg("Audio mode cycled!");
}
});
});
// Card 4: Preferences Toggle
cols[3].group(|ui| {
ui.set_min_height(110.0);
ui.vertical_centered(|ui| {
ui.label(egui::RichText::new("SETTINGS").font(FontId::proportional(13.0)).strong().color(Color32::from_rgb(148, 163, 184)));
let codec_info = format!("{} / {}fps", self.config.video_codec.to_uppercase(), self.config.fps);
ui.label(egui::RichText::new(codec_info).font(FontId::proportional(11.0)).color(Color32::from_rgb(100, 116, 139)));
ui.add_space(8.0);
let gear_label = if self.show_settings { "Hide Panel" } else { "Open Panel" };
if ui.add_sized([120.0, 32.0], egui::Button::new(gear_label).fill(Color32::from_rgb(51, 65, 85)).corner_radius(CornerRadius::same(8_u8))).clicked() {
self.show_settings = !self.show_settings;
}
});
});
});
// Detailed Settings Panel (Collapsible / Dynamic)
if self.show_settings {
ui.add_space(14.0);
ui.separator();
ui.add_space(10.0);
egui::ScrollArea::vertical().max_height(240.0).show(ui, |ui| {
ui.label(egui::RichText::new("Precise Tuning & Configuration").strong().color(Color32::from_rgb(203, 213, 225)));
ui.add_space(8.0);
// 1. Replay Duration with Number Input & Pills
ui.horizontal(|ui| {
ui.set_min_width(180.0);
ui.label("Replay Duration (sec):");
ui.add(egui::DragValue::new(&mut self.replay_sec).range(5..=600).suffix(" s"));
ui.add_space(8.0);
if ui.small_button("30s").clicked() { self.replay_sec = 30; }
if ui.small_button("60s").clicked() { self.replay_sec = 60; }
if ui.small_button("120s").clicked() { self.replay_sec = 120; }
if ui.small_button("300s").clicked() { self.replay_sec = 300; }
});
ui.add_space(6.0);
// 2. Video Bitrate with Number Input & Pills
ui.horizontal(|ui| {
ui.set_min_width(180.0);
ui.label("Bitrate (Mbps):");
ui.add(egui::DragValue::new(&mut self.bitrate_mbps).range(2..=120).suffix(" Mbps"));
ui.add_space(8.0);
if ui.small_button("10M").clicked() { self.bitrate_mbps = 10; }
if ui.small_button("20M").clicked() { self.bitrate_mbps = 20; }
if ui.small_button("30M").clicked() { self.bitrate_mbps = 30; }
if ui.small_button("50M").clicked() { self.bitrate_mbps = 50; }
});
ui.add_space(6.0);
// 3. Target Framerate with Number Input & Pills
ui.horizontal(|ui| {
ui.set_min_width(180.0);
ui.label("Framerate (FPS):");
ui.add(egui::DragValue::new(&mut self.target_fps).range(15..=240).suffix(" fps"));
ui.add_space(8.0);
if ui.small_button("30").clicked() { self.target_fps = 30; }
if ui.small_button("60").clicked() { self.target_fps = 60; }
if ui.small_button("120").clicked() { self.target_fps = 120; }
if ui.small_button("144").clicked() { self.target_fps = 144; }
});
ui.add_space(6.0);
// 4. Audio Routing Mode
ui.horizontal(|ui| {
ui.set_min_width(180.0);
ui.label("Audio Routing Mode:");
egui::ComboBox::from_id_salt("audio_routing_cb")
.selected_text(match self.audio_mode_idx {
1 => "Microphone Only",
2 => "Both (System + Mic)",
3 => "Muted",
_ => "System Audio Only",
})
.show_ui(ui, |ui| {
ui.selectable_value(&mut self.audio_mode_idx, 0, "System Audio Only");
ui.selectable_value(&mut self.audio_mode_idx, 1, "Microphone Only");
ui.selectable_value(&mut self.audio_mode_idx, 2, "Both (System + Mic)");
ui.selectable_value(&mut self.audio_mode_idx, 3, "Muted");
});
});
ui.add_space(10.0);
// Save & Apply Button
ui.horizontal(|ui| {
if ui.add_sized([160.0, 30.0], egui::Button::new("Save & Apply").fill(Color32::from_rgb(37, 99, 235)).corner_radius(CornerRadius::same(6_u8))).clicked() {
self.config.replay_duration_sec = self.replay_sec;
self.config.record_bitrate_kbps = self.bitrate_mbps * 1000;
self.config.replay_bitrate_kbps = self.bitrate_mbps * 1000;
self.config.fps = self.target_fps;
self.config.audio_mode = match self.audio_mode_idx {
1 => "mic",
2 => "both",
3 => "muted",
_ => "system",
}.to_string();
let _ = self.config.save();
let _ = ipc::send_command(Command::ReloadConfig);
self.set_msg("Settings applied & saved!");
}
if ui.button("Open Recordings Folder").clicked() {
let dir = self.config.output_directory.clone();
std::thread::spawn(move || {
let _ = std::process::Command::new("xdg-open").arg(dir).spawn();
});
}
});
});
}
});
}
}
pub fn run_egui_overlay() {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("vrec")
.with_inner_size([740.0, 220.0])
.with_min_inner_size([640.0, 180.0])
.with_max_inner_size([820.0, 520.0])
.with_resizable(true)
.with_decorations(false)
.with_transparent(true)
.with_always_on_top(),
..Default::default()
};
let _ = eframe::run_native(
"vrec-overlay",
options,
Box::new(|_cc| Ok(Box::new(VrecOverlayApp::new()))),
);
}