Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src-tauri/src/autostart.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,21 @@ 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::<Vec<_>>().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 {
// 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.
#[cfg(target_os = "linux")]
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
Expand All @@ -157,7 +172,10 @@ 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 {
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))
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
}

#[cfg(target_os = "linux")]
pub fn set(on: bool) -> Result<(), String> {
Expand Down Expand Up @@ -270,6 +288,18 @@ 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(&quoted("/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"));
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]
fn autostart_file_is_written_and_removed() {
let base = std::env::temp_dir().join(format!("dealdrop-autostart-test-{}", std::process::id()));
Expand Down
32 changes: 23 additions & 9 deletions src-tauri/src/stores.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,9 +208,11 @@ async fn send_auth(url: &str, access: &str) -> R<Value> {

pub async fn gog_wishlist(access: &str) -> R<Vec<WishItem>> {
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) {
Expand All @@ -225,9 +227,15 @@ pub async fn gog_wishlist(access: &str) -> R<Vec<WishItem>> {
Ok(out)
}

fn parse_gog_wishlist_ids(v: &Value) -> Vec<String> {
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<Vec<String>> {
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<WishItem> {
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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()]}}}});
Expand Down
7 changes: 6 additions & 1 deletion src-tauri/src/tui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -210,6 +210,11 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let Event::Key(key) = event::read()? else {
continue;
};
// Windows reports the key release too: without this every key would act twice.
// Repeats stay, so holding Down keeps moving.
if key.kind == KeyEventKind::Release {
continue;
}
match key_action(key) {
Action::Quit => break,
Action::Refresh => {
Expand Down
Loading