From bce3cabf9b4fc59a1b126b7f971b16a3e63fa18a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 30 Jun 2025 14:20:42 +0700 Subject: [PATCH 01/57] Friendlier error when glibc too old, and properly declare min macOS --- qt/launcher/Cargo.toml | 3 +++ qt/launcher/mac/Info.plist | 2 +- qt/launcher/src/main.rs | 3 +++ qt/launcher/src/platform/mod.rs | 7 +++++++ qt/launcher/src/platform/unix.rs | 31 +++++++++++++++++++++++++++++++ 5 files changed, 45 insertions(+), 1 deletion(-) diff --git a/qt/launcher/Cargo.toml b/qt/launcher/Cargo.toml index fd6f2230c..32fb15991 100644 --- a/qt/launcher/Cargo.toml +++ b/qt/launcher/Cargo.toml @@ -14,6 +14,9 @@ anyhow.workspace = true camino.workspace = true dirs.workspace = true +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] +libc.workspace = true + [target.'cfg(windows)'.dependencies] windows.workspace = true widestring.workspace = true diff --git a/qt/launcher/mac/Info.plist b/qt/launcher/mac/Info.plist index 59b67605f..6e19087d6 100644 --- a/qt/launcher/mac/Info.plist +++ b/qt/launcher/mac/Info.plist @@ -7,7 +7,7 @@ CFBundleShortVersionString 1.0 LSMinimumSystemVersion - 11 + 12 CFBundleDocumentTypes diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs index c8cd7e052..2df5ccd15 100644 --- a/qt/launcher/src/main.rs +++ b/qt/launcher/src/main.rs @@ -22,6 +22,7 @@ use anki_process::CommandExt as AnkiCommandExt; use anyhow::Context; use anyhow::Result; +use crate::platform::ensure_os_supported; use crate::platform::ensure_terminal_shown; use crate::platform::get_exe_and_resources_dirs; use crate::platform::get_uv_binary_name; @@ -143,6 +144,8 @@ fn run() -> Result<()> { print!("\x1B[2J\x1B[H"); // Clear screen and move cursor to top println!("\x1B[1mAnki Launcher\x1B[0m\n"); + ensure_os_supported()?; + check_versions(&mut state); main_menu_loop(&state)?; diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs index bbb42df10..50a303656 100644 --- a/qt/launcher/src/platform/mod.rs +++ b/qt/launcher/src/platform/mod.rs @@ -128,3 +128,10 @@ pub fn ensure_terminal_shown() -> Result<()> { print!("\x1b]2;Anki Launcher\x07"); Ok(()) } + +pub fn ensure_os_supported() -> Result<()> { + #[cfg(all(unix, not(target_os = "macos")))] + unix::ensure_glibc_supported()?; + + Ok(()) +} diff --git a/qt/launcher/src/platform/unix.rs b/qt/launcher/src/platform/unix.rs index f37ec81eb..235919106 100644 --- a/qt/launcher/src/platform/unix.rs +++ b/qt/launcher/src/platform/unix.rs @@ -65,3 +65,34 @@ pub fn finalize_uninstall() { let mut input = String::new(); let _ = stdin().read_line(&mut input); } + +pub fn ensure_glibc_supported() -> Result<()> { + use std::ffi::CStr; + let get_glibc_version = || -> Option<(u32, u32)> { + let version_ptr = unsafe { libc::gnu_get_libc_version() }; + if version_ptr.is_null() { + return None; + } + + let version_cstr = unsafe { CStr::from_ptr(version_ptr) }; + let version_str = version_cstr.to_str().ok()?; + + // Parse version string (format: "2.36" or "2.36.1") + let version_parts: Vec<&str> = version_str.split('.').collect(); + if version_parts.len() < 2 { + return None; + } + + let major: u32 = version_parts[0].parse().ok()?; + let minor: u32 = version_parts[1].parse().ok()?; + + Some((major, minor)) + }; + + let (major, minor) = get_glibc_version().unwrap_or_default(); + if major < 3 || (major == 2 && minor < 36) { + anyhow::bail!("Anki requires a modern Linux distro with glibc 2.36 or later."); + } + + Ok(()) +} From 0be87b887ec5d2973df92a2f01f19afbc6455c06 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 30 Jun 2025 14:28:37 +0700 Subject: [PATCH 02/57] Add category type Unsure if this helps; the app is not even showing in the screen time list for me at the moment. https://forums.ankiweb.net/t/correct-apple-screen-time-category-macos-ios/62962 --- qt/launcher/mac/Info.plist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qt/launcher/mac/Info.plist b/qt/launcher/mac/Info.plist index 6e19087d6..ac0ab2f09 100644 --- a/qt/launcher/mac/Info.plist +++ b/qt/launcher/mac/Info.plist @@ -8,6 +8,8 @@ 1.0 LSMinimumSystemVersion 12 + LSApplicationCategoryType + public.app-category.education CFBundleDocumentTypes From 7720c7de1aabb2cbcfb137df7d486e12eea28601 Mon Sep 17 00:00:00 2001 From: Matt Brubeck Date: Mon, 30 Jun 2025 02:47:14 -0700 Subject: [PATCH 03/57] Only run `empty_filtered_deck` on filtered decks. (#4139) Fixes #4138. --- rslib/src/decks/remove.rs | 2 +- rslib/src/scheduler/filtered/mod.rs | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/rslib/src/decks/remove.rs b/rslib/src/decks/remove.rs index befb770f8..a3bc78209 100644 --- a/rslib/src/decks/remove.rs +++ b/rslib/src/decks/remove.rs @@ -28,7 +28,7 @@ impl Collection { let card_count = match deck.kind { DeckKind::Normal(_) => self.delete_all_cards_in_normal_deck(deck.id)?, DeckKind::Filtered(_) => { - self.return_all_cards_in_filtered_deck(deck.id)?; + self.return_all_cards_in_filtered_deck(deck)?; 0 } }; diff --git a/rslib/src/scheduler/filtered/mod.rs b/rslib/src/scheduler/filtered/mod.rs index ad7979e3c..331e54e5d 100644 --- a/rslib/src/scheduler/filtered/mod.rs +++ b/rslib/src/scheduler/filtered/mod.rs @@ -64,7 +64,8 @@ impl Collection { pub fn empty_filtered_deck(&mut self, did: DeckId) -> Result> { self.transact(Op::EmptyFilteredDeck, |col| { - col.return_all_cards_in_filtered_deck(did) + let deck = col.get_deck(did)?.or_not_found(did)?; + col.return_all_cards_in_filtered_deck(&deck) }) } @@ -78,8 +79,11 @@ impl Collection { } impl Collection { - pub(crate) fn return_all_cards_in_filtered_deck(&mut self, did: DeckId) -> Result<()> { - let cids = self.storage.all_cards_in_single_deck(did)?; + pub(crate) fn return_all_cards_in_filtered_deck(&mut self, deck: &Deck) -> Result<()> { + if !deck.is_filtered() { + return Err(FilteredDeckError::FilteredDeckRequired.into()); + } + let cids = self.storage.all_cards_in_single_deck(deck.id)?; self.return_cards_to_home_deck(&cids) } @@ -195,7 +199,7 @@ impl Collection { timing, }; - self.return_all_cards_in_filtered_deck(deck.id)?; + self.return_all_cards_in_filtered_deck(deck)?; self.build_filtered_deck(ctx) } From b22b3310d6cbac649b9558a8bc4636235bc5792d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 1 Jul 2025 11:16:39 +0700 Subject: [PATCH 04/57] Revert "Feat/Cmrr target selector (#4116)" This reverts commit ad0dbb563a520baef1a72c0c6b43af845cce8c82. https://forums.ankiweb.net/t/anki-25-06-beta/62271/156 --- proto/anki/scheduler.proto | 25 ----- rslib/src/scheduler/fsrs/retention.rs | 107 +------------------ ts/routes/deck-options/FsrsOptions.svelte | 14 +-- ts/routes/deck-options/SimulatorModal.svelte | 94 +--------------- ts/routes/deck-options/choices.ts | 23 ---- 5 files changed, 8 insertions(+), 255 deletions(-) diff --git a/proto/anki/scheduler.proto b/proto/anki/scheduler.proto index 5e568aa92..1b7d44a83 100644 --- a/proto/anki/scheduler.proto +++ b/proto/anki/scheduler.proto @@ -402,31 +402,6 @@ message SimulateFsrsReviewRequest { repeated float easy_days_percentages = 10; deck_config.DeckConfig.Config.ReviewCardOrder review_order = 11; optional uint32 suspend_after_lapse_count = 12; - // For CMRR - message CMRRTarget { - message Memorized { - float loss_aversion = 1; - }; - - message Stability {}; - - message FutureMemorized { - int32 days = 1; - }; - - message AverageFutureMemorized { - int32 days = 1; - }; - - oneof kind { - Memorized memorized = 1; - Stability stability = 2; - FutureMemorized future_memorized = 3; - AverageFutureMemorized average_future_memorized = 4; - }; - }; - - optional CMRRTarget target = 13; } message SimulateFsrsReviewResponse { diff --git a/rslib/src/scheduler/fsrs/retention.rs b/rslib/src/scheduler/fsrs/retention.rs index 29f6b490d..4c21623bb 100644 --- a/rslib/src/scheduler/fsrs/retention.rs +++ b/rslib/src/scheduler/fsrs/retention.rs @@ -1,9 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use anki_proto::scheduler::simulate_fsrs_review_request::cmrr_target::Kind; use anki_proto::scheduler::SimulateFsrsReviewRequest; use fsrs::extract_simulator_config; -use fsrs::SimulationResult; use fsrs::SimulatorConfig; use fsrs::FSRS; @@ -16,115 +14,14 @@ pub struct ComputeRetentionProgress { pub total: u32, } -pub fn average_r_power_forgetting_curve( - learn_span: usize, - cards: &[fsrs::Card], - offset: f32, - decay: f32, -) -> f32 { - let factor = 0.9_f32.powf(1.0 / decay) - 1.0; - let exp = decay + 1.0; - let den_factor = factor * exp; - - // Closure equivalent to the inner integral function - let integral_calc = |card: &fsrs::Card| -> f32 { - // Performs element-wise: (s / den_factor) * (1.0 + factor * t / s).powf(exp) - let t1 = learn_span as f32 - card.last_date; - let t2 = t1 + offset; - (card.stability / den_factor) * (1.0 + factor * t2 / card.stability).powf(exp) - - (card.stability / den_factor) * (1.0 + factor * t1 / card.stability).powf(exp) - }; - - // Calculate integral difference and divide by time difference element-wise - cards.iter().map(integral_calc).sum::() / offset -} - impl Collection { pub fn compute_optimal_retention(&mut self, req: SimulateFsrsReviewRequest) -> Result { - // Helper macro to wrap the closure for "CMRRTargetFn"s - macro_rules! wrap { - ($f:expr) => { - Some(fsrs::CMRRTargetFn(std::sync::Arc::new($f))) - }; - } - - let target_type = req.target.unwrap().kind; - - let days_to_simulate = req.days_to_simulate as f32; - - let target = match target_type { - Some(Kind::Memorized(_)) => None, - Some(Kind::FutureMemorized(settings)) => { - wrap!(move |SimulationResult { - cards, - cost_per_day, - .. - }, - w| { - let total_cost = cost_per_day.iter().sum::(); - total_cost - / cards.iter().fold(0., |p, c| { - c.retention_on(w, days_to_simulate + settings.days as f32) + p - }) - }) - } - Some(Kind::AverageFutureMemorized(settings)) => { - wrap!(move |SimulationResult { - cards, - cost_per_day, - .. - }, - w| { - let total_cost = cost_per_day.iter().sum::(); - total_cost - / average_r_power_forgetting_curve( - days_to_simulate as usize, - cards, - settings.days as f32, - -w[20], - ) - }) - } - Some(Kind::Stability(_)) => { - wrap!(move |SimulationResult { - cards, - cost_per_day, - .. - }, - w| { - let total_cost = cost_per_day.iter().sum::(); - total_cost - / cards.iter().fold(0., |p, c| { - p + (c.retention_on(w, days_to_simulate) * c.stability) - }) - }) - } - None => None, - }; - let mut anki_progress = self.new_progress_handler::(); let fsrs = FSRS::new(None)?; if req.days_to_simulate == 0 { invalid_input!("no days to simulate") } - let (mut config, cards) = self.simulate_request_to_config(&req)?; - - if let Some(Kind::Memorized(settings)) = target_type { - let loss_aversion = settings.loss_aversion; - - config.relearning_step_transitions[0][0] *= loss_aversion; - config.relearning_step_transitions[1][0] *= loss_aversion; - config.relearning_step_transitions[2][0] *= loss_aversion; - - config.learning_step_transitions[0][0] *= loss_aversion; - config.learning_step_transitions[1][0] *= loss_aversion; - config.learning_step_transitions[2][0] *= loss_aversion; - - config.state_rating_costs[0][0] *= loss_aversion; - config.state_rating_costs[1][0] *= loss_aversion; - config.state_rating_costs[2][0] *= loss_aversion; - } - + let (config, cards) = self.simulate_request_to_config(&req)?; Ok(fsrs .optimal_retention( &config, @@ -137,7 +34,7 @@ impl Collection { .is_ok() }, Some(cards), - target, + None, )? .clamp(0.7, 0.95)) } diff --git a/ts/routes/deck-options/FsrsOptions.svelte b/ts/routes/deck-options/FsrsOptions.svelte index fadaeba67..5ee3a5d17 100644 --- a/ts/routes/deck-options/FsrsOptions.svelte +++ b/ts/routes/deck-options/FsrsOptions.svelte @@ -7,11 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html ComputeRetentionProgress, type ComputeParamsProgress, } from "@generated/anki/collection_pb"; - import { - SimulateFsrsReviewRequest, - SimulateFsrsReviewRequest_CMRRTarget, - SimulateFsrsReviewRequest_CMRRTarget_Memorized, - } from "@generated/anki/scheduler_pb"; + import { SimulateFsrsReviewRequest } from "@generated/anki/scheduler_pb"; import { computeFsrsParams, evaluateParams, @@ -99,14 +95,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html newCardsIgnoreReviewLimit: $newCardsIgnoreReviewLimit, easyDaysPercentages: $config.easyDaysPercentages, reviewOrder: $config.reviewOrder, - target: new SimulateFsrsReviewRequest_CMRRTarget({ - kind: { - case: "memorized", - value: new SimulateFsrsReviewRequest_CMRRTarget_Memorized({ - lossAversion: 1.6, - }), - }, - }), }); const DESIRED_RETENTION_LOW_THRESHOLD = 0.8; diff --git a/ts/routes/deck-options/SimulatorModal.svelte b/ts/routes/deck-options/SimulatorModal.svelte index ef61054fd..09a05a653 100644 --- a/ts/routes/deck-options/SimulatorModal.svelte +++ b/ts/routes/deck-options/SimulatorModal.svelte @@ -18,30 +18,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import { renderSimulationChart } from "../graphs/simulator"; import { computeOptimalRetention, simulateFsrsReview } from "@generated/backend"; import { runWithBackendProgress } from "@tslib/progress"; - import { - SimulateFsrsReviewRequest_CMRRTarget_AverageFutureMemorized, - SimulateFsrsReviewRequest_CMRRTarget_FutureMemorized, - SimulateFsrsReviewRequest_CMRRTarget_Memorized, - SimulateFsrsReviewRequest_CMRRTarget_Stability, - type ComputeOptimalRetentionResponse, - type SimulateFsrsReviewRequest, - type SimulateFsrsReviewResponse, + import type { + ComputeOptimalRetentionResponse, + SimulateFsrsReviewRequest, + SimulateFsrsReviewResponse, } from "@generated/anki/scheduler_pb"; import type { DeckOptionsState } from "./lib"; import SwitchRow from "$lib/components/SwitchRow.svelte"; import GlobalLabel from "./GlobalLabel.svelte"; import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte"; - import { - DEFAULT_CMRR_TARGET, - CMRRTargetChoices, - reviewOrderChoices, - } from "./choices"; + import { reviewOrderChoices } from "./choices"; import EnumSelectorRow from "$lib/components/EnumSelectorRow.svelte"; import { DeckConfig_Config_LeechAction } from "@generated/anki/deck_config_pb"; import EasyDaysInput from "./EasyDaysInput.svelte"; import Warning from "./Warning.svelte"; import type { ComputeRetentionProgress } from "@generated/anki/collection_pb"; - import Item from "$lib/components/Item.svelte"; import Modal from "bootstrap/js/dist/modal"; export let state: DeckOptionsState; @@ -50,45 +41,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let openHelpModal: (key: string) => void; export let onPresetChange: () => void; - let cmrrTargetType = DEFAULT_CMRR_TARGET; - // All added types must be updated in the proceeding switch statement. - let lastCmrrTargetType = cmrrTargetType; - $: if (simulateFsrsRequest?.target && cmrrTargetType !== lastCmrrTargetType) { - switch (cmrrTargetType) { - case "memorized": - simulateFsrsRequest.target.kind = { - case: "memorized", - value: new SimulateFsrsReviewRequest_CMRRTarget_Memorized({ - lossAversion: 1.6, - }), - }; - break; - case "stability": - simulateFsrsRequest.target.kind = { - case: "stability", - value: new SimulateFsrsReviewRequest_CMRRTarget_Stability({}), - }; - break; - case "futureMemorized": - simulateFsrsRequest.target.kind = { - case: "futureMemorized", - value: new SimulateFsrsReviewRequest_CMRRTarget_FutureMemorized({ - days: 365, - }), - }; - break; - case "averageFutureMemorized": - simulateFsrsRequest.target.kind = { - case: "averageFutureMemorized", - value: new SimulateFsrsReviewRequest_CMRRTarget_AverageFutureMemorized( - { days: 365 }, - ), - }; - break; - } - lastCmrrTargetType = cmrrTargetType; - } - const config = state.currentConfig; let simulateSubgraph: SimulateSubgraph = SimulateSubgraph.count; let tableData: TableDatum[] = []; @@ -470,42 +422,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html {#if computingRetention}
{computeRetentionProgressString}
{/if} - - - - - {"Target: "} - - - - - {#if simulateFsrsRequest.target?.kind.case === "memorized"} - - - {"Fail Cost Multiplier: "} - - - {/if} - - {#if simulateFsrsRequest.target?.kind.case === "futureMemorized" || simulateFsrsRequest.target?.kind.case === "averageFutureMemorized"} - - - {"Days after simulation end: "} - - - {/if} + + {#if optimalRetention} + {estimatedRetention(optimalRetention)} + {#if optimalRetention - $config.desiredRetention >= 0.01} + + {/if} + {/if} + {#if computingRetention} - {tr.actionsCancel()} - {:else} - {tr.deckConfigComputeButton()} +
{computeRetentionProgressString}
{/if} - - - {#if optimalRetention} - {estimatedRetention(optimalRetention)} - {#if optimalRetention - $config.desiredRetention >= 0.01} - - {/if} - {/if} - - {#if computingRetention} -
{computeRetentionProgressString}
- {/if} - - + + - {#if false} - + {#if state.legacyEvaluate}