From 938c55ca01101775b6a30aa657079249677c2d3c Mon Sep 17 00:00:00 2001 From: Kris Cherven <50562493+krischerven@users.noreply.github.com> Date: Wed, 19 Mar 2025 10:58:42 +0000 Subject: [PATCH 1/5] Fix broken window decorations on unpackaged GNOME instances (#3858) * Fix broken window decorations on unpackaged GNOME instances * Fix CONTRIBUTORS detection * Fix CONTRIBUTORS --- pylib/anki/utils.py | 4 ++++ qt/aqt/__init__.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index b5382e6df..1b4212620 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -248,6 +248,10 @@ is_mac = sys.platform.startswith("darwin") is_win = sys.platform.startswith("win32") # also covers *BSD is_lin = not is_mac and not is_win +is_gnome = ( + "gnome" in os.getenv("XDG_CURRENT_DESKTOP", "").lower() + or "gnome" in os.getenv("DESKTOP_SESSION", "").lower() +) dev_mode = os.getenv("ANKIDEV", "") hmr_mode = os.getenv("HMR", "") diff --git a/qt/aqt/__init__.py b/qt/aqt/__init__.py index af3797049..fb9222521 100644 --- a/qt/aqt/__init__.py +++ b/qt/aqt/__init__.py @@ -59,7 +59,7 @@ from anki._backend import RustBackend from anki.buildinfo import version as _version from anki.collection import Collection from anki.consts import HELP_SITE -from anki.utils import checksum, is_lin, is_mac +from anki.utils import checksum, is_gnome, is_lin, is_mac from aqt import gui_hooks from aqt.log import setup_logging from aqt.qt import * @@ -614,7 +614,7 @@ def _run(argv: list[str] | None = None, exec: bool = True) -> AnkiApp | None: ) wayland_forced = os.getenv("ANKI_WAYLAND") - if packaged and wayland_configured: + if (packaged or is_gnome) and wayland_configured: if wayland_forced or not x11_available: # Work around broken fractional scaling in Wayland # https://bugreports.qt.io/browse/QTBUG-113574 From ab8692a91e1c22c4fcad4fd4bf484f6a24ce46ae Mon Sep 17 00:00:00 2001 From: Kris Cherven <50562493+krischerven@users.noreply.github.com> Date: Wed, 19 Mar 2025 11:16:51 +0000 Subject: [PATCH 2/5] Show "and others" at the end of the contributor list in the About dialog (#3863) * Show "and others" at the end of the contributor list in the about dialog * Make about addendum translatable * Fix CONTRIBUTORS * Fix CONTRIBUTORS * Update ftl/qt/about.ftl (dae) --- ftl/qt/about.ftl | 2 ++ qt/aqt/about.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ftl/qt/about.ftl b/ftl/qt/about.ftl index 6fd190638..b841e0843 100644 --- a/ftl/qt/about.ftl +++ b/ftl/qt/about.ftl @@ -8,3 +8,5 @@ about-if-you-have-contributed-and-are = If you have contributed and are not on t about-version = Version { $val } about-visit-website = Visit website about-written-by-damien-elmes-with-patches = Written by Damien Elmes, with patches, translation, testing and design from:

{ $cont } +# appended to the end of the contributor list in the about screen +about-and-others = and others diff --git a/qt/aqt/about.py b/qt/aqt/about.py index 659e380ea..d0d7157af 100644 --- a/qt/aqt/about.py +++ b/qt/aqt/about.py @@ -222,7 +222,7 @@ def show(mw: aqt.AnkiQt) -> QDialog: ) abouttext += "

" + tr.about_written_by_damien_elmes_with_patches( - cont=", ".join(allusers) + cont=", ".join(allusers) + f", {tr.about_and_others()}" ) abouttext += f"

{tr.about_if_you_have_contributed_and_are()}" abouttext += f"

{tr.about_a_big_thanks_to_all_the()}" From d8c83ac075c3587da853a1aeef16300ff27fc357 Mon Sep 17 00:00:00 2001 From: llama <100429699+iamllama@users.noreply.github.com> Date: Wed, 19 Mar 2025 19:56:17 +0800 Subject: [PATCH 3/5] Loosen csv metadata parsing (#3862) * add qsv-sniffer crate * use qsv-sniffer before falling back to old delimiter heuristic * update test metadata macro * revert impl * trim potential suffixed delimiters from non-freeform meta lines * add test --- rslib/src/import_export/text/csv/metadata.rs | 55 +++++++++++++++++--- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/rslib/src/import_export/text/csv/metadata.rs b/rslib/src/import_export/text/csv/metadata.rs index a69acfe9e..7e2f64f5e 100644 --- a/rslib/src/import_export/text/csv/metadata.rs +++ b/rslib/src/import_export/text/csv/metadata.rs @@ -121,19 +121,34 @@ impl Collection { } fn parse_meta_value(&mut self, key: &str, value: &str, metadata: &mut CsvMetadata) { + // trim potential delimiters past the first char* if + // metadata line was mistakenly exported as a record + // *to allow cases like #separator:, + // ASSUMPTION: delimiters are not ascii-alphanumeric + let trimmed_value = value + .char_indices() + .nth(1) + .and_then(|(i, _)| { + value[i..] // SAFETY: char_indices are on char boundaries + .find(|c| !char::is_ascii_alphanumeric(&c)) + .map(|j| value.split_at(i + j).0) + }) + .unwrap_or(value); + match key.trim().to_ascii_lowercase().as_str() { "separator" => { - if let Some(delimiter) = delimiter_from_value(value) { + if let Some(delimiter) = delimiter_from_value(trimmed_value) { metadata.delimiter = delimiter as i32; metadata.force_delimiter = true; } } "html" => { - if let Ok(is_html) = value.to_lowercase().parse() { + if let Ok(is_html) = trimmed_value.to_lowercase().parse() { metadata.is_html = is_html; metadata.force_is_html = true; } } + // freeform values cannot be trimmed thus without knowing the exact delimiter "tags" => metadata.global_tags = collect_tags(value), "columns" => { if let Ok(columns) = parse_columns(value, metadata.delimiter()) { @@ -151,22 +166,22 @@ impl Collection { } } "notetype column" => { - if let Ok(n) = value.trim().parse() { + if let Ok(n) = trimmed_value.trim().parse() { metadata.notetype = Some(CsvNotetype::NotetypeColumn(n)); } } "deck column" => { - if let Ok(n) = value.trim().parse() { + if let Ok(n) = trimmed_value.trim().parse() { metadata.deck = Some(CsvDeck::DeckColumn(n)); } } "tags column" => { - if let Ok(n) = value.trim().parse() { + if let Ok(n) = trimmed_value.trim().parse() { metadata.tags_column = n; } } "guid column" => { - if let Ok(n) = value.trim().parse() { + if let Ok(n) = trimmed_value.trim().parse() { metadata.guid_column = n; } } @@ -891,4 +906,32 @@ pub(in crate::import_export) mod test { maybe_set_tags_column(&mut metadata, &meta_columns); assert_eq!(metadata.tags_column, 4); } + + #[test] + fn should_allow_non_freeform_metadata_lines_to_be_suffixed_by_delimiters() { + let mut col = Collection::new(); + let metadata = metadata!( + col, + r#" +#separator:Pipe,,,,,,, +#html:true||||| +#tags:foo bar::世界,,, +#guid column:8 +#tags column:123abc + "# + .trim() + ); + assert_eq!(metadata.delimiter(), Delimiter::Pipe); + assert!(metadata.is_html); + assert_eq!(metadata.guid_column, 8); + // tags is freeform, potential delimiters aren't trimmed + assert_eq!(metadata.global_tags, ["foo", "bar::世界,,,"]); + // ascii alphanumerics aren't trimmed away + assert_eq!(metadata.tags_column, 0); + + assert_eq!( + metadata!(col, "#separator:\t|,:\n").delimiter(), + Delimiter::Tab + ); + } } From 5d7f6b25c0272e64923c83246b97cc3c8be22b34 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 20 Mar 2025 15:02:40 +0800 Subject: [PATCH 4/5] Improve performance of stats revlog entries with memory state (#3866) * improve performace of stats_revlog_entries_with_memory_state * format * move Vec into FsrsItemForMemoryState --- rslib/src/scheduler/fsrs/memory_state.rs | 3 ++ rslib/src/stats/card.rs | 44 ++++++++++++++---------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/rslib/src/scheduler/fsrs/memory_state.rs b/rslib/src/scheduler/fsrs/memory_state.rs index 5efef07a6..90920f4bb 100644 --- a/rslib/src/scheduler/fsrs/memory_state.rs +++ b/rslib/src/scheduler/fsrs/memory_state.rs @@ -242,6 +242,7 @@ pub(crate) struct FsrsItemForMemoryState { /// When revlogs have been truncated, this stores the initial state at first /// review pub starting_state: Option, + pub filtered_revlogs: Vec, } /// Like [fsrs_item_for_memory_state], but for updating multiple cards at once. @@ -330,6 +331,7 @@ pub(crate) fn fsrs_item_for_memory_state( Ok(Some(FsrsItemForMemoryState { item, starting_state: None, + filtered_revlogs: output.filtered_revlogs, })) } else if let Some(first_user_grade) = output.filtered_revlogs.first() { // the revlog has been truncated, but not fully @@ -356,6 +358,7 @@ pub(crate) fn fsrs_item_for_memory_state( Ok(Some(FsrsItemForMemoryState { item, starting_state: Some(starting_state), + filtered_revlogs: output.filtered_revlogs, })) } else { // only manual and rescheduled revlogs; treat like empty diff --git a/rslib/src/stats/card.rs b/rslib/src/stats/card.rs index 1db368445..48fdc3457 100644 --- a/rslib/src/stats/card.rs +++ b/rslib/src/stats/card.rs @@ -4,6 +4,7 @@ use fsrs::FSRS; use crate::card::CardType; +use crate::card::FsrsMemoryState; use crate::prelude::*; use crate::revlog::RevlogEntry; use crate::scheduler::fsrs::memory_state::fsrs_item_for_memory_state; @@ -140,26 +141,31 @@ impl Collection { let ignore_before = ignore_revlogs_before_ms_from_config(&config)?; let mut result = Vec::new(); - let mut accumulated_revlog = Vec::new(); - - for entry in revlog { - accumulated_revlog.push(entry.clone()); - let item = fsrs_item_for_memory_state( - &fsrs, - accumulated_revlog.clone(), - next_day_at, - historical_retention, - ignore_before, - )?; - let mut card_clone = card.clone(); - card_clone.set_memory_state(&fsrs, item, historical_retention)?; - - let mut stats_entry = stats_revlog_entry(&entry); - stats_entry.memory_state = card_clone.memory_state.map(Into::into); - result.push(stats_entry); + if let Some(item) = fsrs_item_for_memory_state( + &fsrs, + revlog.clone(), + next_day_at, + historical_retention, + ignore_before, + )? { + let memory_states = fsrs.historical_memory_states(item.item, item.starting_state)?; + let mut revlog_index = 0; + for entry in revlog { + let mut stats_entry = stats_revlog_entry(&entry); + let memory_state: FsrsMemoryState = + if entry.id == item.filtered_revlogs[revlog_index].id { + revlog_index += 1; + memory_states[revlog_index - 1].into() + } else { + memory_states[revlog_index].into() + }; + stats_entry.memory_state = Some(memory_state.into()); + result.push(stats_entry); + } + Ok(result.into_iter().rev().collect()) + } else { + Ok(revlog.iter().map(stats_revlog_entry).collect()) } - - Ok(result.into_iter().rev().collect()) } } From d52889f45c3f7999a45fd2dde367f79f3bc3bad4 Mon Sep 17 00:00:00 2001 From: Jarrett Ye Date: Thu, 20 Mar 2025 15:04:38 +0800 Subject: [PATCH 5/5] Feat/simplified relearning steps logic with updated FSRS training API (#3867) * Feat/simplified relearning steps logic with updated FSRS training API * Update params.rs * use ComputeParametersInput * update fsrs-rs dependency * update cargo/format/rust-toolchain --- Cargo.lock | 2 +- Cargo.toml | 2 +- cargo/format/rust-toolchain.toml | 2 +- rslib/src/scheduler/fsrs/params.rs | 51 +++++++++++++++++------------- rslib/src/scheduler/service/mod.rs | 19 +++++++---- 5 files changed, 45 insertions(+), 31 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d6651800..32f15ab2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2099,7 +2099,7 @@ dependencies = [ [[package]] name = "fsrs" version = "3.0.0" -source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=22f8e453c120f5bc5996f86558a559c6b7abfc49#22f8e453c120f5bc5996f86558a559c6b7abfc49" +source = "git+https://github.com/open-spaced-repetition/fsrs-rs.git?rev=08d90d1363b0c4722422bf0ef71ed8fd7d053f8a#08d90d1363b0c4722422bf0ef71ed8fd7d053f8a" dependencies = [ "burn", "itertools 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index e3299a740..c16294236 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,7 +37,7 @@ rev = "184b2ca50ed39ca43da13f0b830a463861adb9ca" [workspace.dependencies.fsrs] # version = "=2.0.3" git = "https://github.com/open-spaced-repetition/fsrs-rs.git" -rev = "22f8e453c120f5bc5996f86558a559c6b7abfc49" +rev = "08d90d1363b0c4722422bf0ef71ed8fd7d053f8a" # path = "../open-spaced-repetition/fsrs-rs" [workspace.dependencies] diff --git a/cargo/format/rust-toolchain.toml b/cargo/format/rust-toolchain.toml index 66a834c36..42af1fe66 100644 --- a/cargo/format/rust-toolchain.toml +++ b/cargo/format/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "nightly-2023-09-02" +channel = "nightly-2025-03-20" profile = "minimal" components = ["rustfmt"] diff --git a/rslib/src/scheduler/fsrs/params.rs b/rslib/src/scheduler/fsrs/params.rs index f304c6e63..2bc3338eb 100644 --- a/rslib/src/scheduler/fsrs/params.rs +++ b/rslib/src/scheduler/fsrs/params.rs @@ -14,6 +14,7 @@ use anki_proto::stats::DeckEntry; use chrono::NaiveDate; use chrono::NaiveTime; use fsrs::CombinedProgressState; +use fsrs::ComputeParametersInput; use fsrs::FSRSItem; use fsrs::FSRSReview; use fsrs::MemoryState; @@ -107,34 +108,40 @@ impl Collection { let (progress, progress_thread) = create_progress_thread()?; let fsrs = FSRS::new(None)?; - let mut params = fsrs.compute_parameters(items.clone(), Some(progress.clone()), true)?; + let mut params = fsrs.compute_parameters(ComputeParametersInput { + train_set: items.clone(), + progress: Some(progress.clone()), + enable_short_term: true, + num_relearning_steps: Some(num_of_relearning_steps), + })?; progress_thread.join().ok(); if let Ok(fsrs) = FSRS::new(Some(current_params)) { let current_rmse = fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; let optimized_fsrs = FSRS::new(Some(¶ms))?; let optimized_rmse = optimized_fsrs.evaluate(items.clone(), |_| true)?.rmse_bins; if current_rmse <= optimized_rmse { - params = current_params.to_vec(); - } - if num_of_relearning_steps > 1 { - let memory_state = MemoryState { - stability: 1.0, - difficulty: 1.0, - }; - let s_fail = optimized_fsrs - .next_states(Some(memory_state), 0.9, 2)? - .again; - let mut s_short_term = s_fail.memory; - for _ in 0..num_of_relearning_steps { - s_short_term = optimized_fsrs - .next_states(Some(s_short_term), 0.9, 0)? - .good - .memory; - } - if s_short_term.stability > memory_state.stability { - let (progress, progress_thread) = create_progress_thread()?; - params = fsrs.compute_parameters(items.clone(), Some(progress), false)?; - progress_thread.join().ok(); + if num_of_relearning_steps <= 1 { + params = current_params.to_vec(); + } else { + let current_fsrs = FSRS::new(Some(current_params))?; + let memory_state = MemoryState { + stability: 1.0, + difficulty: 1.0, + }; + + let s_fail = current_fsrs.next_states(Some(memory_state), 0.9, 2)?.again; + let mut s_short_term = s_fail.memory; + + for _ in 0..num_of_relearning_steps { + s_short_term = current_fsrs + .next_states(Some(s_short_term), 0.9, 0)? + .good + .memory; + } + + if s_short_term.stability < memory_state.stability { + params = current_params.to_vec(); + } } } } diff --git a/rslib/src/scheduler/service/mod.rs b/rslib/src/scheduler/service/mod.rs index e77dce6b3..d398ae65b 100644 --- a/rslib/src/scheduler/service/mod.rs +++ b/rslib/src/scheduler/service/mod.rs @@ -17,6 +17,7 @@ use anki_proto::scheduler::FuzzDeltaResponse; use anki_proto::scheduler::GetOptimalRetentionParametersResponse; use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewResponse; +use fsrs::ComputeParametersInput; use fsrs::FSRSItem; use fsrs::FSRSReview; use fsrs::FSRS; @@ -352,11 +353,12 @@ impl crate::services::BackendSchedulerService for Backend { ) -> Result { let fsrs = FSRS::new(None)?; let fsrs_items = req.items.len() as u32; - let params = fsrs.compute_parameters( - req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(), - None, - true, - )?; + let params = fsrs.compute_parameters(ComputeParametersInput { + train_set: req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(), + progress: None, + enable_short_term: true, + num_relearning_steps: None, + })?; Ok(ComputeFsrsParamsResponse { params, fsrs_items }) } @@ -370,7 +372,12 @@ impl crate::services::BackendSchedulerService for Backend { .into_iter() .map(fsrs_item_proto_to_fsrs) .collect(); - let params = fsrs.benchmark(train_set, true); + let params = fsrs.benchmark(ComputeParametersInput { + train_set, + progress: None, + enable_short_term: true, + num_relearning_steps: None, + }); Ok(FsrsBenchmarkResponse { params }) }