From f41b827b41cad9ac08a28a3cbfa4c4e50a2ef98a Mon Sep 17 00:00:00 2001 From: oiupoyt Date: Fri, 9 Oct 2026 07:47:38 +0530 Subject: [PATCH] feat(ui): screenshot resolution presets, low-end hardware optimizations, and all 8 keybind tests --- src/bin/scythe-daemon.rs | 6 +++- src/bin/scythe-ui.rs | 26 ++++++++++++++---- src/config.rs | 5 ++++ src/encoder.rs | 8 +++++- src/hotkey.rs | 17 ++++++++++++ src/overlay_egui.rs | 59 ++++++++++++++++++++++++++-------------- tests/e2e_test.sh | 10 +++++++ 7 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/bin/scythe-daemon.rs b/src/bin/scythe-daemon.rs index 6da7fb2..5262cab 100644 --- a/src/bin/scythe-daemon.rs +++ b/src/bin/scythe-daemon.rs @@ -481,7 +481,11 @@ async fn main() -> Result<(), Box> { let audio_time_base = audio_codec_ctx_ptr.map(|p| unsafe { (*(p as *mut ffmpeg_next::ffi::AVCodecContext)).time_base }); let ring_capacity = if config.replay_enabled { - (config.replay_duration_sec as usize) * (config.fps as usize + 50) + 60 + if config.record_low_power { + (config.replay_duration_sec as usize) * (config.fps as usize + 15) + 30 + } else { + (config.replay_duration_sec as usize) * (config.fps as usize + 50) + 60 + } } else { 1 }; diff --git a/src/bin/scythe-ui.rs b/src/bin/scythe-ui.rs index aaa7476..5317670 100644 --- a/src/bin/scythe-ui.rs +++ b/src/bin/scythe-ui.rs @@ -322,12 +322,10 @@ fn handle_screenshot() -> Result<(), Box> { grim.args(["-t", "jpeg", "-q", "90"]); } grim.arg(&out_path); - let res = grim.status(); - if res.is_ok() && out_path.exists() { - show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot); - return Ok(()); + let _ = grim.status(); + if !out_path.exists() { + let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status(); } - let _ = std::process::Command::new("import").args(["-window", "root"]).arg(&out_path).status(); } #[cfg(target_os = "windows")] @@ -346,6 +344,24 @@ fn handle_screenshot() -> Result<(), Box> { .status(); } + let scale_dims = match cfg.screenshot_resolution.as_str() { + "720p" => Some("1280:720"), + "1080p" => Some("1920:1080"), + "1440p" => Some("2560:1440"), + "2160p" => Some("3840:2160"), + _ => None, + }; + if let Some(scale) = scale_dims + && out_path.exists() { + let tmp_path = dir.join(format!("tmp_{}", filename)); + if std::fs::rename(&out_path, &tmp_path).is_ok() { + let _ = std::process::Command::new("ffmpeg") + .args(["-v", "error", "-y", "-i", &tmp_path.to_string_lossy(), "-vf", &format!("scale={}:flags=lanczos", scale), &out_path.to_string_lossy()]) + .status(); + let _ = std::fs::remove_file(&tmp_path); + } + } + show_shadowplay_toast("SCREENSHOT", &format!("Saved to {}", filename), ToastIcon::Screenshot); Ok(()) } diff --git a/src/config.rs b/src/config.rs index 969a9e0..bac0ad9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -37,6 +37,7 @@ fn default_screenshot_preset() -> String { "balanced".to_string() } fn default_pause_hotkey() -> String { "Ctrl+Shift+F8".to_string() } fn default_mute_mic_hotkey() -> String { "Ctrl+Shift+M".to_string() } fn default_mute_sys_hotkey() -> String { "Ctrl+Shift+N".to_string() } +fn default_screenshot_resolution() -> String { "native".to_string() } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct ScytheConfig { @@ -154,6 +155,8 @@ pub struct ScytheConfig { pub mute_mic_hotkey: String, #[serde(default = "default_mute_sys_hotkey")] pub mute_sys_hotkey: String, + #[serde(default = "default_screenshot_resolution")] + pub screenshot_resolution: String, #[serde(default)] pub use_monospace_font: bool, } @@ -223,6 +226,7 @@ impl Default for ScytheConfig { pause_hotkey: default_pause_hotkey(), mute_mic_hotkey: default_mute_mic_hotkey(), mute_sys_hotkey: default_mute_sys_hotkey(), + screenshot_resolution: default_screenshot_resolution(), use_monospace_font: false, } } @@ -514,6 +518,7 @@ mod tests { assert_eq!(parsed.pause_hotkey, "Ctrl+Shift+F8"); assert_eq!(parsed.mute_mic_hotkey, "Ctrl+Shift+M"); assert_eq!(parsed.mute_sys_hotkey, "Ctrl+Shift+N"); + assert_eq!(parsed.screenshot_resolution, "native"); assert!(!parsed.use_monospace_font); } diff --git a/src/encoder.rs b/src/encoder.rs index 061991a..8ac84d5 100644 --- a/src/encoder.rs +++ b/src/encoder.rs @@ -104,10 +104,12 @@ impl VaapiEncoder { _ => FF_PROFILE_H264_HIGH, }; (*codec_ctx).flags |= AV_CODEC_FLAG_GLOBAL_HEADER as libc::c_int; - let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), 4, 0); + let async_depth = if low_power { 2 } else { 4 }; + let _ = av_opt_set_int((*codec_ctx).priv_data, c"async_depth".as_ptr(), async_depth, 0); if low_power { let _ = av_opt_set_int((*codec_ctx).priv_data, c"low_power".as_ptr(), 1, 0); + let _ = av_opt_set_int((*codec_ctx).priv_data, c"compression_level".as_ptr(), 1, 0); } // Rec.709 Color Space & Color Range @@ -592,11 +594,15 @@ impl WindowsHwEncoder { let rc = if is_cbr { c"cbr".as_ptr() } else { c"vbr_latency".as_ptr() }; let _ = av_opt_set((*ctx).priv_data, c"usage".as_ptr(), c"ultralowlatency".as_ptr(), 0); let _ = av_opt_set((*ctx).priv_data, c"rc".as_ptr(), rc, 0); + if low_power { + let _ = av_opt_set((*ctx).priv_data, c"quality".as_ptr(), c"speed".as_ptr(), 0); + } } else if desc.contains("QuickSync") { let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"veryfast".as_ptr(), 0); } else { let _ = av_opt_set((*ctx).priv_data, c"preset".as_ptr(), c"ultrafast".as_ptr(), 0); let _ = av_opt_set((*ctx).priv_data, c"tune".as_ptr(), c"zerolatency".as_ptr(), 0); + (*ctx).thread_count = 0; } let ret = avcodec_open2(ctx, c, ptr::null_mut()); diff --git a/src/hotkey.rs b/src/hotkey.rs index b3763b6..97c736c 100644 --- a/src/hotkey.rs +++ b/src/hotkey.rs @@ -95,5 +95,22 @@ mod tests { assert!(parse_hotkey("").is_none()); assert!(parse_hotkey("invalid_key_xyz").is_none()); } + + #[test] + fn test_all_eight_default_scythe_hotkeys() { + let defaults = [ + ("Alt+Z", "menu"), + ("Ctrl+Shift+R", "save_replay"), + ("Ctrl+Shift+F9", "record_toggle"), + ("Ctrl+Shift+F8", "pause_record"), + ("Ctrl+Shift+S", "screenshot"), + ("Ctrl+Shift+F10", "toggle_cursor"), + ("Ctrl+Shift+M", "mute_mic"), + ("Ctrl+Shift+N", "mute_sys"), + ]; + for (hk_str, label) in defaults { + assert!(parse_hotkey(hk_str).is_some(), "Hotkey '{}' for '{}' must parse successfully", hk_str, label); + } + } } diff --git a/src/overlay_egui.rs b/src/overlay_egui.rs index 040d17b..2a579a8 100644 --- a/src/overlay_egui.rs +++ b/src/overlay_egui.rs @@ -1163,6 +1163,7 @@ pub struct ScytheOverlayApp { pub autostart_power_only: bool, pub screenshot_quality: u32, pub screenshot_preset: String, + pub screenshot_resolution: String, pub use_monospace_font: bool, } @@ -1257,6 +1258,7 @@ impl ScytheOverlayApp { let autostart_power_only = config.autostart_power_only; let screenshot_quality = config.screenshot_quality; let screenshot_preset = config.screenshot_preset.clone(); + let screenshot_resolution = config.screenshot_resolution.clone(); let use_monospace_font = config.use_monospace_font; let update_status = Arc::new(std::sync::Mutex::new(crate::updater::UpdateStatus::Idle)); @@ -1339,6 +1341,7 @@ impl ScytheOverlayApp { autostart_power_only, screenshot_quality, screenshot_preset, + screenshot_resolution, use_monospace_font, } } @@ -1391,6 +1394,7 @@ impl ScytheOverlayApp { self.config.autostart_power_only = self.autostart_power_only; self.config.screenshot_quality = self.screenshot_quality; self.config.screenshot_preset = self.screenshot_preset.clone(); + self.config.screenshot_resolution = self.screenshot_resolution.clone(); self.config.use_monospace_font = self.use_monospace_font; let _ = self.config.save(); crate::hyprland_binds::register_hyprland_binds(&self.config); @@ -1437,6 +1441,7 @@ impl ScytheOverlayApp { self.autostart_power_only = def.autostart_power_only; self.screenshot_quality = def.screenshot_quality; self.screenshot_preset = def.screenshot_preset; + self.screenshot_resolution = def.screenshot_resolution; self.use_monospace_font = def.use_monospace_font; self.show_hud_notification("DEFAULTS", "Settings restored to defaults", crate::overlay::ToastIcon::Info); } @@ -1481,6 +1486,24 @@ impl ScytheOverlayApp { .status(); } + let scale_dims = match self.screenshot_resolution.as_str() { + "720p" => Some("1280:720"), + "1080p" => Some("1920:1080"), + "1440p" => Some("2560:1440"), + "2160p" => Some("3840:2160"), + _ => None, + }; + if let Some(scale) = scale_dims + && out_path.exists() { + let tmp_path = dir.join(format!("tmp_{}", fname)); + if std::fs::rename(&out_path, &tmp_path).is_ok() { + let _ = std::process::Command::new("ffmpeg") + .args(["-v", "error", "-y", "-i", &tmp_path.to_string_lossy(), "-vf", &format!("scale={}:flags=lanczos", scale), &out_path.to_string_lossy()]) + .status(); + let _ = std::fs::remove_file(&tmp_path); + } + } + self.show_hud_notification("SCREENSHOT", &format!("Saved to {}", fname), crate::overlay::ToastIcon::Screenshot); } @@ -2308,14 +2331,6 @@ impl ScytheOverlayApp { }); ui.add_space(8.0); - ui.horizontal(|ui| { - let mut sfg = self.save_folder_by_game; - if toggle_switch(ui, &mut sfg, accent).changed() { - self.save_folder_by_game = sfg; - } - ui.label(egui::RichText::new("Save video in a folder based on the games name").size(11.5).color(Color32::WHITE)); - }); - ui.add_space(4.0); ui.horizontal(|ui| { let mut nbg = self.name_by_game; if toggle_switch(ui, &mut nbg, accent).changed() { @@ -2734,20 +2749,19 @@ impl ScytheOverlayApp { }); ui.add_space(8.0); + ui.label(egui::RichText::new("Resolution preset:").size(11.5).color(Color32::from_rgb(180, 185, 190))); + ui.add_space(4.0); ui.horizontal(|ui| { - ui.label(egui::RichText::new("Compression quality:").size(11.5).color(Color32::from_rgb(180, 185, 190))); - let mut q = self.screenshot_quality; - if ui.add(egui::Slider::new(&mut q, 10..=100).suffix("%")).changed() { - self.screenshot_quality = q; - } - }); - - ui.add_space(8.0); - ui.horizontal(|ui| { - ui.label(egui::RichText::new("Compression preset:").size(11.5).color(Color32::from_rgb(180, 185, 190))); - for (p_id, label) in [("fast", "Fast"), ("balanced", "Balanced"), ("max", "Maximum Compression")] { - if squared_button(ui, label, self.screenshot_preset == p_id, accent) { - self.screenshot_preset = p_id.to_string(); + let presets = [ + ("native", "Native (Source)"), + ("1080p", "1080p (FHD)"), + ("1440p", "1440p (QHD)"), + ("2160p", "2160p (4K UHD)"), + ("720p", "720p (HD)"), + ]; + for (res_id, label) in presets { + if squared_button(ui, label, self.screenshot_resolution == res_id, accent) { + self.screenshot_resolution = res_id.to_string(); } } }); @@ -4432,6 +4446,7 @@ mod tests { app.config.audio_codec = "opus".to_string(); app.record_low_power = true; app.show_replay_notifications = false; + app.screenshot_resolution = "1080p".to_string(); app.save_and_apply_settings(); @@ -4449,6 +4464,7 @@ mod tests { assert_eq!(app.config.audio_codec, "opus"); assert!(app.config.record_low_power); assert!(!app.config.show_replay_notifications); + assert_eq!(app.config.screenshot_resolution, "1080p"); app.reset_settings_to_defaults(); assert_eq!(app.container_format, "mp4"); @@ -4463,5 +4479,6 @@ mod tests { assert_eq!(app.config.audio_codec, "opus"); assert!(!app.record_low_power); assert!(app.show_replay_notifications); + assert_eq!(app.screenshot_resolution, "native"); } } diff --git a/tests/e2e_test.sh b/tests/e2e_test.sh index c06abd2..25e3e8d 100755 --- a/tests/e2e_test.sh +++ b/tests/e2e_test.sh @@ -16,6 +16,13 @@ rm -f record_*.mp4 replay_*.mp4 SAVE_DIR=$(python3 -c 'import json, os; p=os.path.expanduser("~/.config/scythe/config.json"); print(os.path.expanduser(json.load(open(p))["output_directory"])) if os.path.exists(p) else print(os.path.expanduser("~/Videos/Scythe"))' 2>/dev/null || echo "$HOME/Videos/Scythe") mkdir -p "$SAVE_DIR" +WAS_SERVICE_ACTIVE=0 +if systemctl --user is-active --quiet scythe-daemon.service 2>/dev/null; then + WAS_SERVICE_ACTIVE=1 + systemctl --user stop scythe-daemon.service 2>/dev/null || true + sleep 0.5 +fi + echo "[2/6] Starting scythe-daemon with mock capture..." target/release/scythe-daemon --mock > daemon_test.log 2>&1 & DAEMON_PID=$! @@ -25,6 +32,9 @@ cleanup() { target/release/scythe-ui --quit 2>/dev/null || kill -9 $DAEMON_PID 2>/dev/null || true fi rm -f daemon_test.log + if [ "$WAS_SERVICE_ACTIVE" -eq 1 ]; then + systemctl --user start scythe-daemon.service 2>/dev/null || true + fi } trap cleanup EXIT