From 0b7173de322ae074def569dc6eae5b6366905233 Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Sat, 7 Nov 2026 02:06:54 +0100 Subject: [PATCH 1/2] Autostart, TUI keys and GOG wishlist: three silent failures - Settings no longer shows "Start when I log in" as on after the desktop's own autostart panel switched DealDrop off (Hidden=true / X-GNOME-Autostart-enabled=false). - The terminal UI ignores key releases, so on Windows one key press no longer acts twice. - GOG wishlist: a failed profile call fails the wishlist instead of quietly pricing it in US/USD, and an answer in a shape we don't know shows an error instead of an empty wishlist. Co-Authored-By: Claude Opus 5.5 --- src-tauri/src/autostart.rs | 21 ++++++++++++++++++++- src-tauri/src/stores.rs | 32 +++++++++++++++++++++++--------- src-tauri/src/tui.rs | 6 +++++- 3 files changed, 48 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs index c41616a..ebe2fca 100644 --- a/src-tauri/src/autostart.rs +++ b/src-tauri/src/autostart.rs @@ -133,6 +133,14 @@ fn repaired_entry(text: &str, exec: &str, exists: impl Fn(&str) -> bool) -> Opti Some(text.lines().map(|l| if l.starts_with("Exec=") { exec } else { l }).collect::>().join("\n") + "\n") } +/// False when the desktop's own autostart settings switched our entry off: those keep the file and add +/// `Hidden=true` / `X-GNOME-Autostart-enabled=false`, so the file existing is not "on". Turning it on +/// again rewrites the whole entry, which drops them. +#[cfg(any(target_os = "linux", test))] +fn entry_enabled(text: &str) -> bool { + !text.lines().map(str::trim).any(|l| l == "Hidden=true" || l == "X-GNOME-Autostart-enabled=false") +} + /// Held across reading and writing the entry, so the startup repair and the Settings toggle can't interleave. #[cfg(target_os = "linux")] static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(()); @@ -157,7 +165,9 @@ pub fn refresh() { pub fn refresh() {} #[cfg(target_os = "linux")] -pub fn get() -> bool { config_home().is_some_and(|c| desktop_path(&c).exists()) } +pub fn get() -> bool { + config_home().and_then(|c| std::fs::read_to_string(desktop_path(&c)).ok()).is_some_and(|text| entry_enabled(&text)) +} #[cfg(target_os = "linux")] pub fn set(on: bool) -> Result<(), String> { @@ -270,6 +280,15 @@ mod tests { assert!(exec_path(" X-Exec=\"/apps/x\"").is_none()); } + #[test] + fn a_desktop_entry_switched_off_by_the_desktop_is_not_on() { + let text = desktop_entry("ed("/opt/dealdrop"), Path::new("/i.png")); + assert!(entry_enabled(&text)); + assert!(!entry_enabled(&(text.clone() + "Hidden=true\n"))); + assert!(!entry_enabled(&text.replace("enabled=true", "enabled=false"))); + assert!(entry_enabled("Hidden=false\nX-GNOME-Autostart-enabled=true\n")); + } + #[test] fn autostart_file_is_written_and_removed() { let base = std::env::temp_dir().join(format!("dealdrop-autostart-test-{}", std::process::id())); diff --git a/src-tauri/src/stores.rs b/src-tauri/src/stores.rs index 7bf98b6..6d77f40 100644 --- a/src-tauri/src/stores.rs +++ b/src-tauri/src/stores.rs @@ -208,9 +208,11 @@ async fn send_auth(url: &str, access: &str) -> R { pub async fn gog_wishlist(access: &str) -> R> { let v = send_auth("https://embed.gog.com/user/wishlist.json", access).await?; - let ids = parse_gog_wishlist_ids(&v); + let ids = parse_gog_wishlist_ids(&v)?; if ids.is_empty() { return Ok(vec![]); } - let (_, country, currency, _) = gog_user(access).await; + // A failed profile call must fail the wishlist (the caller keeps the last good rows), not quietly + // re-price everything in US/USD: that would pass the USD-only checks on cheapest offers and targets. + let (_, country, currency, _) = parse_gog_user(&send_auth("https://embed.gog.com/userData.json", access).await?); let mut out = vec![]; for chunk in ids.chunks(50) { @@ -225,9 +227,15 @@ pub async fn gog_wishlist(access: &str) -> R> { Ok(out) } -fn parse_gog_wishlist_ids(v: &Value) -> Vec { - v["wishlist"].as_object().into_iter().flatten() - .filter(|(_, on)| **on != false && **on != 0).map(|(k, _)| k.clone()).collect() +/// An object of ids (value false/0 = not wanted). An empty wishlist may come back as `[]`; any other +/// shape is an answer we don't understand, so the wishlist fails and the UI shows it instead of going empty. +fn parse_gog_wishlist_ids(v: &Value) -> R> { + match &v["wishlist"] { + Value::Object(map) => Ok(map.iter() + .filter(|(_, on)| **on != false && **on != 0).map(|(k, _)| k.clone()).collect()), + Value::Array(a) if a.is_empty() => Ok(vec![]), + _ => Err("GOG sent a wishlist DealDrop doesn't understand".into()), + } } fn parse_gog_wishlist_products(products: &Value, prices: &Value) -> Vec { @@ -493,11 +501,17 @@ mod unit { #[test] fn gog_wishlist_ids_skip_off_entries() { let v = json!({"wishlist": {"10": true, "20": false, "30": 0, "40": 1}}); - let mut ids = super::parse_gog_wishlist_ids(&v); + let mut ids = super::parse_gog_wishlist_ids(&v).unwrap(); ids.sort(); assert_eq!(ids, vec!["10".to_string(), "40".to_string()]); - assert!(super::parse_gog_wishlist_ids(&json!({})).is_empty()); - assert!(super::parse_gog_wishlist_ids(&json!({"wishlist": []})).is_empty()); + // An empty wishlist can come back as [] — a real empty list, not a broken answer. + assert!(super::parse_gog_wishlist_ids(&json!({"wishlist": []})).unwrap().is_empty()); + // Anything else is a shape we don't know: the wishlist must fail, not go silently empty. + let e = "GOG sent a wishlist DealDrop doesn't understand"; + assert_eq!(super::parse_gog_wishlist_ids(&json!({})).unwrap_err(), e); + assert_eq!(super::parse_gog_wishlist_ids(&json!({"wishlist": null})).unwrap_err(), e); + assert_eq!(super::parse_gog_wishlist_ids(&json!({"wishlist": ["10"]})).unwrap_err(), e); + assert_eq!(super::parse_gog_wishlist_ids(&serde_json::Value::Null).unwrap_err(), e); } #[test] @@ -813,7 +827,7 @@ mod unit { let prices = json!({"_embedded": {"items": [{"_embedded": {"product": {"id": 7}, "prices": [{"basePrice": "999 USD", "finalPrice": "500 USD"}]}}]}}); for m in mutations(&products) { for w in super::parse_gog_wishlist_products(&m, &prices) { sane("gog products", &w.price_info); } } for m in mutations(&prices) { for w in super::parse_gog_wishlist_products(&products, &m) { sane("gog prices", &w.price_info); } } - for m in mutations(&json!({"wishlist": {"7": true, "8": 0}})) { super::parse_gog_wishlist_ids(&m); } + for m in mutations(&json!({"wishlist": {"7": true, "8": 0}})) { let _ = super::parse_gog_wishlist_ids(&m); } let offer = json!({"title": "G", "id": "o1", "offerId": "o1", "productSlug": "g/home", "keyImages": [{"type": "Thumbnail", "url": "u"}], "price": {"totalPrice": {"discountPrice": 500, "originalPrice": 1000, "currencyInfo": {"decimals": 2}, "fmtPrice": {"discountPrice": "$5.00"}}}}); let sales = json!({"data": {"Catalog": {"searchStore": {"elements": [offer.clone()]}}}}); diff --git a/src-tauri/src/tui.rs b/src-tauri/src/tui.rs index ec89c9c..8a5a3d4 100644 --- a/src-tauri/src/tui.rs +++ b/src-tauri/src/tui.rs @@ -2,7 +2,7 @@ use crate::sources::{self, Deal, Free}; use crate::stores; -use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent}; +use ratatui::crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind}; use ratatui::crossterm::{ execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, @@ -210,6 +210,10 @@ pub fn run() -> Result<(), Box> { let Event::Key(key) = event::read()? else { continue; }; + // Windows reports the key release too: without this every key would act twice. + if key.kind != KeyEventKind::Press { + continue; + } match key_action(key) { Action::Quit => break, Action::Refresh => { From 46ae44eb677f15dab521d2395c8e89c9e640e817 Mon Sep 17 00:00:00 2001 From: SusieeTheFemboy <96988330+SusieeTheLinuxUser@users.noreply.github.com> Date: Sat, 7 Nov 2026 02:31:40 +0100 Subject: [PATCH 2/2] Autostart: read only [Desktop Entry] keys, allow spaces around =; TUI keeps key repeats From CodeRabbit's review: - Hidden = true (spaces are allowed by the spec) now counts as off, and a Hidden=true in another group (e.g. a desktop action) no longer does. - get() takes the same lock as refresh() and set(). - The TUI skips only key releases, so holding Up/Down keeps moving. Co-Authored-By: Claude Opus 5.5 --- src-tauri/src/autostart.rs | 13 ++++++++++++- src-tauri/src/tui.rs | 3 ++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/autostart.rs b/src-tauri/src/autostart.rs index ebe2fca..9a89617 100644 --- a/src-tauri/src/autostart.rs +++ b/src-tauri/src/autostart.rs @@ -138,7 +138,14 @@ fn repaired_entry(text: &str, exec: &str, exists: impl Fn(&str) -> bool) -> Opti /// again rewrites the whole entry, which drops them. #[cfg(any(target_os = "linux", test))] fn entry_enabled(text: &str) -> bool { - !text.lines().map(str::trim).any(|l| l == "Hidden=true" || l == "X-GNOME-Autostart-enabled=false") + // Only `[Desktop Entry]` keys count (an action group may say Hidden=true), and the spec + // allows spaces around `=`. + let mut in_entry = true; + !text.lines().map(str::trim).any(|l| { + if l.starts_with('[') { in_entry = l == "[Desktop Entry]"; return false; } + let Some((key, value)) = l.split_once('=') else { return false }; + in_entry && matches!((key.trim(), value.trim()), ("Hidden", "true") | ("X-GNOME-Autostart-enabled", "false")) + }) } /// Held across reading and writing the entry, so the startup repair and the Settings toggle can't interleave. @@ -166,6 +173,7 @@ pub fn refresh() {} #[cfg(target_os = "linux")] pub fn get() -> bool { + let _lock = LOCK.lock(); config_home().and_then(|c| std::fs::read_to_string(desktop_path(&c)).ok()).is_some_and(|text| entry_enabled(&text)) } @@ -287,6 +295,9 @@ mod tests { assert!(!entry_enabled(&(text.clone() + "Hidden=true\n"))); assert!(!entry_enabled(&text.replace("enabled=true", "enabled=false"))); assert!(entry_enabled("Hidden=false\nX-GNOME-Autostart-enabled=true\n")); + assert!(!entry_enabled(&(text.clone() + "Hidden = true\n"))); + assert!(!entry_enabled(&text.replace("enabled=true", "enabled = false"))); + assert!(entry_enabled(&(text.clone() + "\n[Desktop Action Quit]\nHidden=true\n"))); } #[test] diff --git a/src-tauri/src/tui.rs b/src-tauri/src/tui.rs index 8a5a3d4..af93aab 100644 --- a/src-tauri/src/tui.rs +++ b/src-tauri/src/tui.rs @@ -211,7 +211,8 @@ pub fn run() -> Result<(), Box> { continue; }; // Windows reports the key release too: without this every key would act twice. - if key.kind != KeyEventKind::Press { + // Repeats stay, so holding Down keeps moving. + if key.kind == KeyEventKind::Release { continue; } match key_action(key) {