two[/latex]b", false),
(
- format!(
- "a

b",
- fname
- )
- .into(),
+ format!("a

b").into(),
vec![ExtractedLatex {
fname: fname.into(),
latex: "one\nand\ntwo".into()
diff --git a/rslib/src/log.rs b/rslib/src/log.rs
index 4fb4dcfaf..fedc597c4 100644
--- a/rslib/src/log.rs
+++ b/rslib/src/log.rs
@@ -69,8 +69,8 @@ fn maybe_rotate_log(path: &str) -> io::Result<()> {
return Ok(());
}
- let path2 = format!("{}.1", path);
- let path3 = format!("{}.2", path);
+ let path2 = format!("{path}.1");
+ let path3 = format!("{path}.2");
// if a rotated file already exists, rename it
if let Err(e) = fs::rename(&path2, path3) {
diff --git a/rslib/src/media/files.rs b/rslib/src/media/files.rs
index 9fd3bc85f..6974e2f81 100644
--- a/rslib/src/media/files.rs
+++ b/rslib/src/media/files.rs
@@ -218,7 +218,7 @@ fn truncate_filename(fname: &str, max_bytes: usize) -> Cow
{
let mut new_name = if ext.is_empty() {
stem.to_string()
} else {
- format!("{}.{}", stem, ext)
+ format!("{stem}.{ext}")
};
// make sure we don't break Windows by ending with a space or dot
diff --git a/rslib/src/notes/mod.rs b/rslib/src/notes/mod.rs
index 2b53321b9..932022e99 100644
--- a/rslib/src/notes/mod.rs
+++ b/rslib/src/notes/mod.rs
@@ -270,7 +270,7 @@ impl Note {
self.fields
.last_mut()
.unwrap()
- .push_str(&format!("; {}", last));
+ .push_str(&format!("; {last}"));
}
}
}
diff --git a/rslib/src/notetype/schema11.rs b/rslib/src/notetype/schema11.rs
index 272456ab7..8d713cbe7 100644
--- a/rslib/src/notetype/schema11.rs
+++ b/rslib/src/notetype/schema11.rs
@@ -126,7 +126,7 @@ fn other_to_bytes(other: &HashMap) -> Vec {
} else {
serde_json::to_vec(other).unwrap_or_else(|e| {
// theoretically should never happen
- println!("serialization failed for {:?}: {}", other, e);
+ println!("serialization failed for {other:?}: {e}");
vec![]
})
}
@@ -140,7 +140,7 @@ pub(crate) fn parse_other_fields(
Default::default()
} else {
let mut map: HashMap = serde_json::from_slice(bytes).unwrap_or_else(|e| {
- println!("deserialization failed for other: {}", e);
+ println!("deserialization failed for other: {e}");
Default::default()
});
map.retain(|k, _v| !reserved.contains(k));
diff --git a/rslib/src/notetype/stock.rs b/rslib/src/notetype/stock.rs
index f17f6b949..9b5df66d5 100644
--- a/rslib/src/notetype/stock.rs
+++ b/rslib/src/notetype/stock.rs
@@ -179,8 +179,8 @@ pub(crate) fn cloze(tr: &I18n) -> Notetype {
let back_extra = tr.notetypes_back_extra_field();
config = nt.add_field(back_extra.as_ref());
config.tag = Some(ClozeField::BackExtra as u32);
- let qfmt = format!("{{{{cloze:{}}}}}", text);
- let afmt = format!("{}
\n{{{{{}}}}}", qfmt, back_extra);
+ let qfmt = format!("{{{{cloze:{text}}}}}");
+ let afmt = format!("{qfmt}
\n{{{{{back_extra}}}}}");
nt.add_template(nt.name.clone(), qfmt, afmt);
nt
}
diff --git a/rslib/src/scheduler/answering/mod.rs b/rslib/src/scheduler/answering/mod.rs
index 8ae1518d0..ce6720d3d 100644
--- a/rslib/src/scheduler/answering/mod.rs
+++ b/rslib/src/scheduler/answering/mod.rs
@@ -889,22 +889,20 @@ pub(crate) mod test {
) -> Result<()> {
// Change due time to fake card answer_time,
// works since answer_time is calculated as due - last_ivl
- let update_due_string = format!("update cards set due={}", shift_due_time);
+ let update_due_string = format!("update cards set due={shift_due_time}");
col.storage.db.execute_batch(&update_due_string)?;
col.clear_study_queues();
let current_card_state = current_state(col, post_answer.card_id);
let state = match current_card_state {
CardState::Normal(NormalState::Learning(state)) => state,
- _ => panic!("State is not Normal: {:?}", current_card_state),
+ _ => panic!("State is not Normal: {current_card_state:?}"),
};
let elapsed_secs = state.elapsed_secs as i32;
// Give a 1 second leeway when the test runs on the off chance
// that the test runs as a second rolls over.
assert!(
(elapsed_secs - expected_elapsed_secs).abs() <= 1,
- "elapsed_secs: {} != expected_elapsed_secs: {}",
- elapsed_secs,
- expected_elapsed_secs
+ "elapsed_secs: {elapsed_secs} != expected_elapsed_secs: {expected_elapsed_secs}"
);
Ok(())
diff --git a/rslib/src/scheduler/filtered/mod.rs b/rslib/src/scheduler/filtered/mod.rs
index f1f3cc07d..ad7979e3c 100644
--- a/rslib/src/scheduler/filtered/mod.rs
+++ b/rslib/src/scheduler/filtered/mod.rs
@@ -214,14 +214,14 @@ impl Collection {
.search_terms
.get_mut(0)
.unwrap();
- term1.search = format!("{} is:due", search);
+ term1.search = format!("{search} is:due");
let term2 = deck
.filtered_mut()
.unwrap()
.search_terms
.get_mut(1)
.unwrap();
- term2.search = format!("{} is:new", search);
+ term2.search = format!("{search} is:new");
}
}
diff --git a/rslib/src/scheduler/timespan.rs b/rslib/src/scheduler/timespan.rs
index c779d33bc..b015e3e1e 100644
--- a/rslib/src/scheduler/timespan.rs
+++ b/rslib/src/scheduler/timespan.rs
@@ -25,7 +25,7 @@ pub fn answer_button_time_collapsible(seconds: u32, collapse_secs: u32, tr: &I18
if seconds == 0 {
tr.scheduling_end().into()
} else if seconds < collapse_secs {
- format!("<{}", string)
+ format!("<{string}")
} else {
string
}
diff --git a/rslib/src/search/builder.rs b/rslib/src/search/builder.rs
index 452f4d832..a76af0560 100644
--- a/rslib/src/search/builder.rs
+++ b/rslib/src/search/builder.rs
@@ -219,7 +219,7 @@ impl From for SearchNode {
impl From for SearchNode {
fn from(n: NoteId) -> Self {
- SearchNode::NoteIds(format!("{}", n))
+ SearchNode::NoteIds(format!("{n}"))
}
}
diff --git a/rslib/src/search/mod.rs b/rslib/src/search/mod.rs
index 63096dad8..ff21bf4ca 100644
--- a/rslib/src/search/mod.rs
+++ b/rslib/src/search/mod.rs
@@ -240,7 +240,7 @@ impl Collection {
} else {
self.storage.setup_searched_cards_table()?;
}
- let sql = format!("insert into search_cids {}", sql);
+ let sql = format!("insert into search_cids {sql}");
let cards = self
.storage
@@ -307,7 +307,7 @@ impl Collection {
let (sql, args) = writer.build_query(&top_node, mode.required_table())?;
self.storage.setup_searched_notes_table()?;
- let sql = format!("insert into search_nids {}", sql);
+ let sql = format!("insert into search_nids {sql}");
let notes = self
.storage
diff --git a/rslib/src/search/parser.rs b/rslib/src/search/parser.rs
index 041ec4948..409862fce 100644
--- a/rslib/src/search/parser.rs
+++ b/rslib/src/search/parser.rs
@@ -277,7 +277,7 @@ fn unquoted_term(s: &str) -> IResult {
Err(parse_failure(
s,
FailKind::UnknownEscape {
- provided: format!("\\{}", c),
+ provided: format!("\\{c}"),
},
))
} else if "\"() \u{3000}".contains(s.chars().next().unwrap()) {
@@ -637,7 +637,7 @@ fn check_id_list<'a>(s: &'a str, context: &str) -> ParseResult<'a, &'a str> {
s,
// id lists are undocumented, so no translation
FailKind::Other {
- info: Some(format!("expected only digits and commas in {}:", context)),
+ info: Some(format!("expected only digits and commas in {context}:")),
},
))
}
@@ -1110,19 +1110,19 @@ mod test {
for term in &["added", "edited", "rated", "resched"] {
assert!(matches!(
- failkind(&format!("{}:1.1", term)),
+ failkind(&format!("{term}:1.1")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
assert!(matches!(
- failkind(&format!("{}:-1", term)),
+ failkind(&format!("{term}:-1")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
assert!(matches!(
- failkind(&format!("{}:", term)),
+ failkind(&format!("{term}:")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
assert!(matches!(
- failkind(&format!("{}:foo", term)),
+ failkind(&format!("{term}:foo")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
}
@@ -1223,19 +1223,19 @@ mod test {
for term in &["ivl", "reps", "lapses", "pos"] {
assert!(matches!(
- failkind(&format!("prop:{}>", term)),
+ failkind(&format!("prop:{term}>")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
assert!(matches!(
- failkind(&format!("prop:{}=0.5", term)),
+ failkind(&format!("prop:{term}=0.5")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
assert!(matches!(
- failkind(&format!("prop:{}!=-1", term)),
+ failkind(&format!("prop:{term}!=-1")),
SearchErrorKind::InvalidPositiveWholeNumber { .. }
));
assert!(matches!(
- failkind(&format!("prop:{} {
},
SearchNode::Deck(deck) => self.write_deck(&norm(deck))?,
SearchNode::NotetypeId(ntid) => {
- write!(self.sql, "n.mid = {}", ntid).unwrap();
+ write!(self.sql, "n.mid = {ntid}").unwrap();
}
SearchNode::DeckIdsWithoutChildren(dids) => {
write!(
self.sql,
- "c.did in ({}) or (c.odid != 0 and c.odid in ({}))",
- dids, dids
+ "c.did in ({dids}) or (c.odid != 0 and c.odid in ({dids}))"
)
.unwrap();
}
@@ -175,13 +174,13 @@ impl SqlWriter<'_> {
SearchNode::Tag { tag, is_re } => self.write_tag(&norm(tag), *is_re),
SearchNode::State(state) => self.write_state(state)?,
SearchNode::Flag(flag) => {
- write!(self.sql, "(c.flags & 7) == {}", flag).unwrap();
+ write!(self.sql, "(c.flags & 7) == {flag}").unwrap();
}
SearchNode::NoteIds(nids) => {
write!(self.sql, "{} in ({})", self.note_id_column(), nids).unwrap();
}
SearchNode::CardIds(cids) => {
- write!(self.sql, "c.id in ({})", cids).unwrap();
+ write!(self.sql, "c.id in ({cids})").unwrap();
}
SearchNode::Property { operator, kind } => self.write_prop(operator, kind)?,
SearchNode::CustomData(key) => self.write_custom_data(key)?,
@@ -199,7 +198,7 @@ impl SqlWriter<'_> {
text
};
// implicitly wrap in %
- let text = format!("%{}%", text);
+ let text = format!("%{text}%");
self.args.push(text);
let arg_idx = self.args.len();
@@ -279,7 +278,7 @@ impl SqlWriter<'_> {
text => {
write!(self.sql, "n.tags regexp ?").unwrap();
let re = &to_custom_re(text, r"\S");
- self.args.push(format!("(?i).* {}(::| ).*", re));
+ self.args.push(format!("(?i).* {re}(::| ).*"));
}
}
}
@@ -293,10 +292,10 @@ impl SqlWriter<'_> {
write!(self.sql, "c.id in (select cid from revlog where id").unwrap();
match op {
- ">" => write!(self.sql, " >= {}", target_cutoff_ms),
- ">=" => write!(self.sql, " >= {}", day_before_cutoff_ms),
- "<" => write!(self.sql, " < {}", day_before_cutoff_ms),
- "<=" => write!(self.sql, " < {}", target_cutoff_ms),
+ ">" => write!(self.sql, " >= {target_cutoff_ms}"),
+ ">=" => write!(self.sql, " >= {day_before_cutoff_ms}"),
+ "<" => write!(self.sql, " < {day_before_cutoff_ms}"),
+ "<=" => write!(self.sql, " < {target_cutoff_ms}"),
"=" => write!(
self.sql,
" between {} and {}",
@@ -314,7 +313,7 @@ impl SqlWriter<'_> {
.unwrap();
match ease {
- RatingKind::AnswerButton(u) => write!(self.sql, " and ease = {})", u),
+ RatingKind::AnswerButton(u) => write!(self.sql, " and ease = {u})"),
RatingKind::AnyAnswerButton => write!(self.sql, " and ease > 0)"),
RatingKind::ManualReschedule => write!(self.sql, " and ease = 0)"),
}
@@ -356,9 +355,9 @@ impl SqlWriter<'_> {
pos = pos
)
.unwrap(),
- PropertyKind::Interval(ivl) => write!(self.sql, "ivl {} {}", op, ivl).unwrap(),
- PropertyKind::Reps(reps) => write!(self.sql, "reps {} {}", op, reps).unwrap(),
- PropertyKind::Lapses(days) => write!(self.sql, "lapses {} {}", op, days).unwrap(),
+ PropertyKind::Interval(ivl) => write!(self.sql, "ivl {op} {ivl}").unwrap(),
+ PropertyKind::Reps(reps) => write!(self.sql, "reps {op} {reps}").unwrap(),
+ PropertyKind::Lapses(days) => write!(self.sql, "lapses {op} {days}").unwrap(),
PropertyKind::Ease(ease) => {
write!(self.sql, "factor {} {}", op, (ease * 1000.0) as u32).unwrap()
}
@@ -474,7 +473,7 @@ impl SqlWriter<'_> {
};
// convert to a regex that includes child decks
- self.args.push(format!("(?i)^{}($|\x1f)", native_deck));
+ self.args.push(format!("(?i)^{native_deck}($|\x1f)"));
let arg_idx = self.args.len();
self.sql.push_str(&format!(concat!(
"(c.did in (select id from decks where name regexp ?{n})",
@@ -491,7 +490,7 @@ impl SqlWriter<'_> {
let ids = self.col.storage.deck_id_with_children(&parent)?;
let mut buf = String::new();
ids_to_string(&mut buf, &ids);
- write!(self.sql, "c.did in {}", buf,).unwrap();
+ write!(self.sql, "c.did in {buf}",).unwrap();
} else {
self.sql.push_str("false")
}
@@ -502,7 +501,7 @@ impl SqlWriter<'_> {
fn write_template(&mut self, template: &TemplateKind) {
match template {
TemplateKind::Ordinal(n) => {
- write!(self.sql, "c.ord = {}", n).unwrap();
+ write!(self.sql, "c.ord = {n}").unwrap();
}
TemplateKind::Name(name) => {
if is_glob(name) {
@@ -550,7 +549,7 @@ impl SqlWriter<'_> {
}
fn write_all_fields_regexp(&mut self, val: &str) {
- self.args.push(format!("(?i){}", val));
+ self.args.push(format!("(?i){val}"));
write!(self.sql, "regexp_fields(?{}, n.flds)", self.args.len()).unwrap();
}
@@ -566,7 +565,7 @@ impl SqlWriter<'_> {
return Ok(());
}
- self.args.push(format!("(?i){}", val));
+ self.args.push(format!("(?i){val}"));
let arg_idx = self.args.len();
let all_notetype_clauses = field_indicies_by_notetype
@@ -775,13 +774,13 @@ impl SqlWriter<'_> {
fn write_added(&mut self, days: u32) -> Result<()> {
let cutoff = self.previous_day_cutoff(days)?.as_millis();
- write!(self.sql, "c.id > {}", cutoff).unwrap();
+ write!(self.sql, "c.id > {cutoff}").unwrap();
Ok(())
}
fn write_edited(&mut self, days: u32) -> Result<()> {
let cutoff = self.previous_day_cutoff(days)?;
- write!(self.sql, "n.mod > {}", cutoff).unwrap();
+ write!(self.sql, "n.mod > {cutoff}").unwrap();
Ok(())
}
@@ -813,7 +812,7 @@ impl SqlWriter<'_> {
} else {
std::borrow::Cow::Borrowed(word)
};
- self.args.push(format!(r"(?i){}", word));
+ self.args.push(format!(r"(?i){word}"));
let arg_idx = self.args.len();
if let Some(field_indices_by_notetype) = self.included_fields_for_unqualified_regex()? {
let notetype_clause = |ctx: &UnqualifiedRegexSearchContext| -> String {
diff --git a/rslib/src/search/writer.rs b/rslib/src/search/writer.rs
index 600a18fd6..2158bffba 100644
--- a/rslib/src/search/writer.rs
+++ b/rslib/src/search/writer.rs
@@ -70,30 +70,30 @@ fn write_search_node(node: &SearchNode) -> String {
match node {
UnqualifiedText(s) => maybe_quote(&s.replace(':', "\\:")),
SingleField { field, text, is_re } => write_single_field(field, text, *is_re),
- AddedInDays(u) => format!("added:{}", u),
- EditedInDays(u) => format!("edited:{}", u),
- IntroducedInDays(u) => format!("introduced:{}", u),
+ AddedInDays(u) => format!("added:{u}"),
+ EditedInDays(u) => format!("edited:{u}"),
+ IntroducedInDays(u) => format!("introduced:{u}"),
CardTemplate(t) => write_template(t),
- Deck(s) => maybe_quote(&format!("deck:{}", s)),
- DeckIdsWithoutChildren(s) => format!("did:{}", s),
+ Deck(s) => maybe_quote(&format!("deck:{s}")),
+ DeckIdsWithoutChildren(s) => format!("did:{s}"),
// not exposed on the GUI end
DeckIdWithChildren(_) => "".to_string(),
- NotetypeId(NotetypeIdType(i)) => format!("mid:{}", i),
- Notetype(s) => maybe_quote(&format!("note:{}", s)),
+ NotetypeId(NotetypeIdType(i)) => format!("mid:{i}"),
+ Notetype(s) => maybe_quote(&format!("note:{s}")),
Rated { days, ease } => write_rated(days, ease),
Tag { tag, is_re } => write_single_field("tag", tag, *is_re),
Duplicates { notetype_id, text } => write_dupe(notetype_id, text),
State(k) => write_state(k),
- Flag(u) => format!("flag:{}", u),
- NoteIds(s) => format!("nid:{}", s),
- CardIds(s) => format!("cid:{}", s),
+ Flag(u) => format!("flag:{u}"),
+ NoteIds(s) => format!("nid:{s}"),
+ CardIds(s) => format!("cid:{s}"),
Property { operator, kind } => write_property(operator, kind),
WholeCollection => "deck:*".to_string(),
- Regex(s) => maybe_quote(&format!("re:{}", s)),
- NoCombining(s) => maybe_quote(&format!("nc:{}", s)),
- WordBoundary(s) => maybe_quote(&format!("w:{}", s)),
- CustomData(k) => maybe_quote(&format!("has-cd:{}", k)),
- Preset(s) => maybe_quote(&format!("preset:{}", s)),
+ Regex(s) => maybe_quote(&format!("re:{s}")),
+ NoCombining(s) => maybe_quote(&format!("nc:{s}")),
+ WordBoundary(s) => maybe_quote(&format!("w:{s}")),
+ CustomData(k) => maybe_quote(&format!("has-cd:{k}")),
+ Preset(s) => maybe_quote(&format!("preset:{s}")),
}
}
@@ -128,23 +128,23 @@ fn write_single_field(field: &str, text: &str, is_re: bool) -> String {
fn write_template(template: &TemplateKind) -> String {
match template {
TemplateKind::Ordinal(u) => format!("card:{}", u + 1),
- TemplateKind::Name(s) => maybe_quote(&format!("card:{}", s)),
+ TemplateKind::Name(s) => maybe_quote(&format!("card:{s}")),
}
}
fn write_rated(days: &u32, ease: &RatingKind) -> String {
use RatingKind::*;
match ease {
- AnswerButton(n) => format!("rated:{}:{}", days, n),
- AnyAnswerButton => format!("rated:{}", days),
- ManualReschedule => format!("resched:{}", days),
+ AnswerButton(n) => format!("rated:{days}:{n}"),
+ AnyAnswerButton => format!("rated:{days}"),
+ ManualReschedule => format!("resched:{days}"),
}
}
/// Escape double quotes and backslashes: \"
fn write_dupe(notetype_id: &NotetypeId, text: &str) -> String {
let esc = text.replace('\\', r"\\");
- maybe_quote(&format!("dupe:{},{}", notetype_id, esc))
+ maybe_quote(&format!("dupe:{notetype_id},{esc}"))
}
fn write_state(kind: &StateKind) -> String {
@@ -167,19 +167,19 @@ fn write_state(kind: &StateKind) -> String {
fn write_property(operator: &str, kind: &PropertyKind) -> String {
use PropertyKind::*;
match kind {
- Due(i) => format!("prop:due{}{}", operator, i),
- Interval(u) => format!("prop:ivl{}{}", operator, u),
- Reps(u) => format!("prop:reps{}{}", operator, u),
- Lapses(u) => format!("prop:lapses{}{}", operator, u),
- Ease(f) => format!("prop:ease{}{}", operator, f),
- Position(u) => format!("prop:pos{}{}", operator, u),
- Stability(u) => format!("prop:s{}{}", operator, u),
- Difficulty(u) => format!("prop:d{}{}", operator, u),
- Retrievability(u) => format!("prop:r{}{}", operator, u),
+ Due(i) => format!("prop:due{operator}{i}"),
+ Interval(u) => format!("prop:ivl{operator}{u}"),
+ Reps(u) => format!("prop:reps{operator}{u}"),
+ Lapses(u) => format!("prop:lapses{operator}{u}"),
+ Ease(f) => format!("prop:ease{operator}{f}"),
+ Position(u) => format!("prop:pos{operator}{u}"),
+ Stability(u) => format!("prop:s{operator}{u}"),
+ Difficulty(u) => format!("prop:d{operator}{u}"),
+ Retrievability(u) => format!("prop:r{operator}{u}"),
Rated(u, ease) => match ease {
- RatingKind::AnswerButton(val) => format!("prop:rated{}{}:{}", operator, u, val),
- RatingKind::AnyAnswerButton => format!("prop:rated{}{}", operator, u),
- RatingKind::ManualReschedule => format!("prop:resched{}{}", operator, u),
+ RatingKind::AnswerButton(val) => format!("prop:rated{operator}{u}:{val}"),
+ RatingKind::AnyAnswerButton => format!("prop:rated{operator}{u}"),
+ RatingKind::ManualReschedule => format!("prop:resched{operator}{u}"),
},
CustomDataNumber { key, value } => format!("prop:cdn:{key}{operator}{value}"),
CustomDataString { key, value } => {
diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs
index 38cf5ef0f..0205aef0d 100644
--- a/rslib/src/storage/card/mod.rs
+++ b/rslib/src/storage/card/mod.rs
@@ -829,8 +829,7 @@ impl fmt::Display for ReviewOrderSubclause {
ReviewOrderSubclause::RetrievabilitySm2 { today, order } => {
temp_string = format!(
// - (elapsed days+0.001)/(scheduled interval)
- "-(1 + cast({today}-due+0.001 as real)/ivl) {order}",
- today = today
+ "-(1 + cast({today}-due+0.001 as real)/ivl) {order}"
);
&temp_string
}
@@ -844,7 +843,7 @@ impl fmt::Display for ReviewOrderSubclause {
ReviewOrderSubclause::Added => "nid asc, ord asc",
ReviewOrderSubclause::ReverseAdded => "nid desc, ord asc",
};
- write!(f, "{}", clause)
+ write!(f, "{clause}")
}
}
diff --git a/rslib/src/storage/deck/mod.rs b/rslib/src/storage/deck/mod.rs
index 7b1e08d58..d47d03894 100644
--- a/rslib/src/storage/deck/mod.rs
+++ b/rslib/src/storage/deck/mod.rs
@@ -33,7 +33,7 @@ fn row_to_deck(row: &Row) -> Result {
common,
kind: kind.kind.ok_or_else(|| {
AnkiError::db_error(
- format!("invalid deck kind: {}", id),
+ format!("invalid deck kind: {id}"),
DbErrorKind::MissingEntity,
)
})?,
@@ -347,8 +347,8 @@ impl SqliteStorage {
))?;
let top = current.name.as_native_str();
- let prefix_start = &format!("{}\x1f", top);
- let prefix_end = &format!("{}\x20", top);
+ let prefix_start = &format!("{top}\x1f");
+ let prefix_end = &format!("{top}\x20");
self.db
.prepare_cached(include_str!("update_active.sql"))?
@@ -379,7 +379,7 @@ impl SqliteStorage {
let decks = self
.get_schema11_decks()
.map_err(|e| AnkiError::JsonError {
- info: format!("decoding decks: {}", e),
+ info: format!("decoding decks: {e}"),
})?;
let mut names = HashSet::new();
for (_id, deck) in decks {
diff --git a/rslib/src/storage/deckconfig/mod.rs b/rslib/src/storage/deckconfig/mod.rs
index 2103e1512..5cc39cfc8 100644
--- a/rslib/src/storage/deckconfig/mod.rs
+++ b/rslib/src/storage/deckconfig/mod.rs
@@ -197,7 +197,7 @@ impl SqliteStorage {
serde_json::from_value(conf)
})
.map_err(|e| AnkiError::JsonError {
- info: format!("decoding deck config: {}", e),
+ info: format!("decoding deck config: {e}"),
})
})?;
for (id, mut conf) in conf.into_iter() {
diff --git a/rslib/src/storage/mod.rs b/rslib/src/storage/mod.rs
index f240555eb..948bc30e4 100644
--- a/rslib/src/storage/mod.rs
+++ b/rslib/src/storage/mod.rs
@@ -52,7 +52,7 @@ where
{
let mut trailing_sep = false;
for id in ids {
- write!(buf, "{},", id).unwrap();
+ write!(buf, "{id},").unwrap();
trailing_sep = true;
}
if trailing_sep {
diff --git a/rslib/src/storage/notetype/mod.rs b/rslib/src/storage/notetype/mod.rs
index 88c4074ac..692b68887 100644
--- a/rslib/src/storage/notetype/mod.rs
+++ b/rslib/src/storage/notetype/mod.rs
@@ -345,7 +345,7 @@ impl SqliteStorage {
let nts = self
.get_schema11_notetypes()
.map_err(|e| AnkiError::JsonError {
- info: format!("decoding models: {:?}", e),
+ info: format!("decoding models: {e:?}"),
})?;
let mut names = HashSet::new();
for (mut ntid, nt) in nts {
diff --git a/rslib/src/storage/sqlite.rs b/rslib/src/storage/sqlite.rs
index e31fdd46a..e9ae55a3b 100644
--- a/rslib/src/storage/sqlite.rs
+++ b/rslib/src/storage/sqlite.rs
@@ -587,7 +587,7 @@ impl SqliteStorage {
}) {
Ok(corrupt) => corrupt,
Err(e) => {
- println!("error: {:?}", e);
+ println!("error: {e:?}");
true
}
}
diff --git a/rslib/src/storage/sync.rs b/rslib/src/storage/sync.rs
index 4bd0e5242..256566d68 100644
--- a/rslib/src/storage/sync.rs
+++ b/rslib/src/storage/sync.rs
@@ -54,7 +54,7 @@ impl SqliteStorage {
if let Some(new_usn) = server_usn_if_client {
let mut stmt = self
.db
- .prepare_cached(&format!("update {} set usn=? where id=?", table))?;
+ .prepare_cached(&format!("update {table} set usn=? where id=?"))?;
for id in ids {
stmt.execute(params![new_usn, id])?;
}
diff --git a/rslib/src/storage/sync_check.rs b/rslib/src/storage/sync_check.rs
index 50e92f7d0..7693a5921 100644
--- a/rslib/src/storage/sync_check.rs
+++ b/rslib/src/storage/sync_check.rs
@@ -11,7 +11,7 @@ impl SqliteStorage {
fn table_has_usn(&self, table: &str) -> Result {
Ok(self
.db
- .prepare(&format!("select null from {} where usn=-1", table))?
+ .prepare(&format!("select null from {table} where usn=-1"))?
.query([])?
.next()?
.is_some())
@@ -19,7 +19,7 @@ impl SqliteStorage {
fn table_count(&self, table: &str) -> Result {
self.db
- .query_row(&format!("select count() from {}", table), [], |r| r.get(0))
+ .query_row(&format!("select count() from {table}"), [], |r| r.get(0))
.map_err(Into::into)
}
@@ -36,7 +36,7 @@ impl SqliteStorage {
] {
if self.table_has_usn(table)? {
return Err(AnkiError::sync_error(
- format!("table had usn=-1: {}", table),
+ format!("table had usn=-1: {table}"),
SyncErrorKind::Other,
));
}
diff --git a/rslib/src/sync/collection/tests.rs b/rslib/src/sync/collection/tests.rs
index abf82262f..a7aa6cc8d 100644
--- a/rslib/src/sync/collection/tests.rs
+++ b/rslib/src/sync/collection/tests.rs
@@ -100,7 +100,7 @@ where
_lock = LOCK.lock().await;
endpoint
} else {
- format!("http://{}/", addr)
+ format!("http://{addr}/")
};
let endpoint = Url::try_from(endpoint.as_str()).unwrap();
let auth = SyncAuth {
@@ -734,7 +734,7 @@ async fn regular_sync(ctx: &SyncTestContext) -> Result<()> {
for table in &["cards", "notes", "decks"] {
assert_eq!(
col1.storage
- .db_scalar::(&format!("select count() from {}", table))?,
+ .db_scalar::(&format!("select count() from {table}"))?,
2
);
}
@@ -754,7 +754,7 @@ async fn regular_sync(ctx: &SyncTestContext) -> Result<()> {
for table in &["cards", "notes", "decks"] {
assert_eq!(
col2.storage
- .db_scalar::(&format!("select count() from {}", table))?,
+ .db_scalar::(&format!("select count() from {table}"))?,
1
);
}
diff --git a/rslib/src/sync/media/database/client/mod.rs b/rslib/src/sync/media/database/client/mod.rs
index 5fe493679..f9c6e5ed1 100644
--- a/rslib/src/sync/media/database/client/mod.rs
+++ b/rslib/src/sync/media/database/client/mod.rs
@@ -285,7 +285,7 @@ fn row_to_name_and_checksum(row: &Row) -> error::Result<(String, Sha1Hash)> {
fn trace(event: rusqlite::trace::TraceEvent) {
if let rusqlite::trace::TraceEvent::Stmt(_, sql) = event {
- println!("sql: {}", sql);
+ println!("sql: {sql}");
}
}
diff --git a/rslib/src/tags/findreplace.rs b/rslib/src/tags/findreplace.rs
index 5db6e3ed2..b60b5ed88 100644
--- a/rslib/src/tags/findreplace.rs
+++ b/rslib/src/tags/findreplace.rs
@@ -35,7 +35,7 @@ impl Collection {
};
if !match_case {
- search = format!("(?i){}", search).into();
+ search = format!("(?i){search}").into();
}
self.transact(Op::UpdateTag, |col| {
diff --git a/rslib/src/tags/matcher.rs b/rslib/src/tags/matcher.rs
index b4961015e..d3c6ad88b 100644
--- a/rslib/src/tags/matcher.rs
+++ b/rslib/src/tags/matcher.rs
@@ -33,7 +33,7 @@ impl TagMatcher {
(?:^|\ )
# 1: the tag prefix
(
- {}
+ {tags}
)
(?:
# 2: an optional child separator
@@ -41,8 +41,7 @@ impl TagMatcher {
# or a space/end of string the end of the string
|\ |$
)
- "#,
- tags
+ "#
))?;
Ok(Self {
@@ -61,7 +60,7 @@ impl TagMatcher {
let out = self.regex.replace(tag, |caps: &Captures| {
// if we captured the child separator, add it to the replacement
if caps.get(2).is_some() {
- Cow::Owned(format!("{}::", replacement))
+ Cow::Owned(format!("{replacement}::"))
} else {
Cow::Borrowed(replacement)
}
@@ -92,7 +91,7 @@ impl TagMatcher {
let replacement = replacer(caps.get(1).unwrap().as_str());
// if we captured the child separator, add it to the replacement
if caps.get(2).is_some() {
- format!("{}::", replacement)
+ format!("{replacement}::")
} else {
replacement
}
diff --git a/rslib/src/tags/reparent.rs b/rslib/src/tags/reparent.rs
index cbab806ff..4976b760e 100644
--- a/rslib/src/tags/reparent.rs
+++ b/rslib/src/tags/reparent.rs
@@ -109,7 +109,7 @@ fn reparented_name(existing_name: &str, new_parent: Option<&str>) -> Option baz::bar
- let new_name = format!("{}::{}", new_parent, existing_base);
+ let new_name = format!("{new_parent}::{existing_base}");
if new_name != existing_name {
Some(new_name)
} else {
diff --git a/rslib/src/template.rs b/rslib/src/template.rs
index e3a900a2b..4895cc162 100644
--- a/rslib/src/template.rs
+++ b/rslib/src/template.rs
@@ -265,10 +265,8 @@ fn template_error_to_anki_error(
};
let details = htmlescape::encode_minimal(&localized_template_error(tr, err));
let more_info = tr.card_template_rendering_more_info();
- let source = format!(
- "{}
{}
{}",
- header, details, TEMPLATE_ERROR_LINK, more_info
- );
+ let source =
+ format!("{header}
{details}
{more_info}");
AnkiError::TemplateError { info: source }
}
@@ -279,32 +277,29 @@ fn localized_template_error(tr: &I18n, err: TemplateError) -> String {
.card_template_rendering_no_closing_brackets("}}", tag)
.into(),
TemplateError::ConditionalNotClosed(tag) => tr
- .card_template_rendering_conditional_not_closed(format!("{{{{/{}}}}}", tag))
+ .card_template_rendering_conditional_not_closed(format!("{{{{/{tag}}}}}"))
.into(),
TemplateError::ConditionalNotOpen {
closed,
currently_open,
} => if let Some(open) = currently_open {
tr.card_template_rendering_wrong_conditional_closed(
- format!("{{{{/{}}}}}", closed),
- format!("{{{{/{}}}}}", open),
+ format!("{{{{/{closed}}}}}"),
+ format!("{{{{/{open}}}}}"),
)
} else {
tr.card_template_rendering_conditional_not_open(
- format!("{{{{/{}}}}}", closed),
- format!("{{{{#{}}}}}", closed),
- format!("{{{{^{}}}}}", closed),
+ format!("{{{{/{closed}}}}}"),
+ format!("{{{{#{closed}}}}}"),
+ format!("{{{{^{closed}}}}}"),
)
}
.into(),
TemplateError::FieldNotFound { field, filters } => tr
- .card_template_rendering_no_such_field(format!("{{{{{}{}}}}}", filters, field), field)
+ .card_template_rendering_no_such_field(format!("{{{{{filters}{field}}}}}"), field)
.into(),
TemplateError::NoSuchConditional(condition) => tr
- .card_template_rendering_no_such_field(
- format!("{{{{{}}}}}", condition),
- &condition[1..],
- )
+ .card_template_rendering_no_such_field(format!("{{{{{condition}}}}}"), &condition[1..])
.into(),
}
}
@@ -523,10 +518,7 @@ impl RenderContext<'_> {
Ok(false ^ negated)
} else {
let prefix = if negated { "^" } else { "#" };
- Err(TemplateError::NoSuchConditional(format!(
- "{}{}",
- prefix, key
- )))
+ Err(TemplateError::NoSuchConditional(format!("{prefix}{key}")))
}
}
}
@@ -858,14 +850,14 @@ fn nodes_to_string(buf: &mut String, nodes: &[ParsedNode]) {
.unwrap();
}
ParsedNode::Conditional { key, children } => {
- write!(buf, "{{{{#{}}}}}", key).unwrap();
+ write!(buf, "{{{{#{key}}}}}").unwrap();
nodes_to_string(buf, children);
- write!(buf, "{{{{/{}}}}}", key).unwrap();
+ write!(buf, "{{{{/{key}}}}}").unwrap();
}
ParsedNode::NegatedConditional { key, children } => {
- write!(buf, "{{{{^{}}}}}", key).unwrap();
+ write!(buf, "{{{{^{key}}}}}").unwrap();
nodes_to_string(buf, children);
- write!(buf, "{{{{/{}}}}}", key).unwrap();
+ write!(buf, "{{{{/{key}}}}}").unwrap();
}
}
}
diff --git a/rslib/src/template_filters.rs b/rslib/src/template_filters.rs
index cb3504fe3..4949e756d 100644
--- a/rslib/src/template_filters.rs
+++ b/rslib/src/template_filters.rs
@@ -165,15 +165,15 @@ fn furigana_filter(text: &str) -> Cow {
/// convert to [[type:...]] for the gui code to process
fn type_filter<'a>(field_name: &str) -> Cow<'a, str> {
- format!("[[type:{}]]", field_name).into()
+ format!("[[type:{field_name}]]").into()
}
fn type_cloze_filter<'a>(field_name: &str) -> Cow<'a, str> {
- format!("[[type:cloze:{}]]", field_name).into()
+ format!("[[type:cloze:{field_name}]]").into()
}
fn type_nc_filter<'a>(field_name: &str) -> Cow<'a, str> {
- format!("[[type:nc:{}]]", field_name).into()
+ format!("[[type:nc:{field_name}]]").into()
}
fn hint_filter<'a>(text: &'a str, field_name: &str) -> Cow<'a, str> {
@@ -191,18 +191,17 @@ fn hint_filter<'a>(text: &'a str, field_name: &str) -> Cow<'a, str> {
r##"
-{}
-{}
-"##,
- id, field_name, id, text
+{field_name}
+{text}
+"##
)
.into()
}
fn tts_filter(options: &str, text: &str) -> String {
- format!("[anki:tts lang={}]{}[/anki:tts]", options, text)
+ format!("[anki:tts lang={options}]{text}[/anki:tts]")
}
// Tests
diff --git a/rslib/src/text.rs b/rslib/src/text.rs
index f83332ff8..590c05b39 100644
--- a/rslib/src/text.rs
+++ b/rslib/src/text.rs
@@ -484,7 +484,7 @@ pub(crate) fn to_custom_re<'a>(txt: &'a str, wildcard: &str) -> Cow<'a, str> {
match s {
r"\\" | r"\*" => s.to_string(),
r"\_" => "_".to_string(),
- "*" => format!("{}*", wildcard),
+ "*" => format!("{wildcard}*"),
"_" => wildcard.to_string(),
s => regex::escape(s),
}
diff --git a/rust-toolchain.toml b/rust-toolchain.toml
index fa07f7fa5..8a21ec74e 100644
--- a/rust-toolchain.toml
+++ b/rust-toolchain.toml
@@ -1,3 +1,3 @@
[toolchain]
# older versions may fail to compile; newer versions may fail the clippy tests
-channel = "1.87.0"
+channel = "1.88.0"
diff --git a/tools/minilints/src/main.rs b/tools/minilints/src/main.rs
index 2650ec648..37e213570 100644
--- a/tools/minilints/src/main.rs
+++ b/tools/minilints/src/main.rs
@@ -108,7 +108,7 @@ impl LintContext {
LazyCell::force(&self.unstaged_changes);
fix_copyright(path)?;
} else {
- println!("missing standard copyright header: {:?}", path);
+ println!("missing standard copyright header: {path:?}");
self.found_problems = true;
}
}
@@ -241,7 +241,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
.write(true)
.open(path)
.with_context(|| format!("opening {path}"))?;
- write!(file, "{}{}", header, data).with_context(|| format!("writing {path}"))?;
+ write!(file, "{header}{data}").with_context(|| format!("writing {path}"))?;
Ok(())
}
From 731e7d5b5cda2d8eec368d502ab4f456276ccb15 Mon Sep 17 00:00:00 2001
From: Damien Elmes
Date: Sun, 29 Jun 2025 13:32:06 +0700
Subject: [PATCH 7/9] Add uninstall support to launcher
---
Cargo.toml | 2 +-
qt/launcher/src/main.rs | 84 ++++++++++++++++++-
qt/launcher/src/platform/mac.rs | 29 +++++++
qt/launcher/src/platform/mod.rs | 2 +-
qt/launcher/src/platform/unix.rs | 18 ++++
qt/launcher/src/platform/windows.rs | 125 ++++++++++++++++++++++++++++
qt/launcher/win/anki.template.nsi | 14 +++-
7 files changed, 266 insertions(+), 8 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 61cca8649..2c6eee2af 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -138,7 +138,7 @@ unic-ucd-category = "0.9.0"
unicode-normalization = "0.1.24"
walkdir = "2.5.0"
which = "8.0.0"
-winapi = { version = "0.3", features = ["wincon"] }
+winapi = { version = "0.3", features = ["wincon", "winreg"] }
windows = { version = "0.61.3", features = ["Media_SpeechSynthesis", "Media_Core", "Foundation_Collections", "Storage_Streams"] }
wiremock = "0.6.3"
xz2 = "0.1.7"
diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs
index 3cfbca45a..fdfa47e9e 100644
--- a/qt/launcher/src/main.rs
+++ b/qt/launcher/src/main.rs
@@ -37,6 +37,7 @@ struct State {
uv_install_root: std::path::PathBuf,
uv_cache_dir: std::path::PathBuf,
no_cache_marker: std::path::PathBuf,
+ anki_base_folder: std::path::PathBuf,
uv_path: std::path::PathBuf,
user_pyproject_path: std::path::PathBuf,
user_python_version_path: std::path::PathBuf,
@@ -59,6 +60,7 @@ pub enum MainMenuChoice {
Version(VersionKind),
ToggleBetas,
ToggleCache,
+ Uninstall,
Quit,
}
@@ -86,6 +88,7 @@ fn run() -> Result<()> {
uv_install_root: uv_install_root.clone(),
uv_cache_dir: uv_install_root.join("cache"),
no_cache_marker: uv_install_root.join("nocache"),
+ anki_base_folder: get_anki_base_path()?,
uv_path: exe_dir.join(get_uv_binary_name()),
user_pyproject_path: uv_install_root.join("pyproject.toml"),
user_python_version_path: uv_install_root.join(".python-version"),
@@ -95,6 +98,13 @@ fn run() -> Result<()> {
sync_complete_marker: uv_install_root.join(".sync_complete"),
};
+ // Check for uninstall request from Windows uninstaller
+ if std::env::var("ANKI_LAUNCHER_UNINSTALL").is_ok() {
+ ensure_terminal_shown()?;
+ handle_uninstall(&state)?;
+ return Ok(());
+ }
+
// Create install directory and copy project files in
create_dir_all(&state.uv_install_root)?;
let had_user_pyproj = state.user_pyproject_path.exists();
@@ -200,6 +210,12 @@ fn main_menu_loop(state: &State) -> Result<()> {
println!();
continue;
}
+ MainMenuChoice::Uninstall => {
+ if handle_uninstall(state)? {
+ std::process::exit(0);
+ }
+ continue;
+ }
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
// For other choices, update project files and sync
update_pyproject_for_version(
@@ -295,7 +311,9 @@ fn get_main_menu_choice(
"5) Cache downloads: {}",
if cache_enabled { "on" } else { "off" }
);
- println!("6) Quit");
+ println!();
+ println!("6) Uninstall");
+ println!("7) Quit");
print!("> ");
let _ = stdout().flush();
@@ -318,7 +336,8 @@ fn get_main_menu_choice(
}
"4" => MainMenuChoice::ToggleBetas,
"5" => MainMenuChoice::ToggleCache,
- "6" => MainMenuChoice::Quit,
+ "6" => MainMenuChoice::Uninstall,
+ "7" => MainMenuChoice::Quit,
_ => {
println!("Invalid input. Please try again.");
continue;
@@ -378,6 +397,9 @@ fn update_pyproject_for_version(
MainMenuChoice::ToggleCache => {
unreachable!();
}
+ MainMenuChoice::Uninstall => {
+ unreachable!();
+ }
MainMenuChoice::Version(version_kind) => {
let content = read_file(&dist_pyproject_path)?;
let content_str =
@@ -494,7 +516,7 @@ fn inject_helper_addon(_uv_install_root: &std::path::Path) -> Result<()> {
Ok(())
}
-fn get_anki_addons21_path() -> Result {
+fn get_anki_base_path() -> Result {
let anki_base_path = if cfg!(target_os = "windows") {
// Windows: %APPDATA%\Anki2
dirs::config_dir()
@@ -512,7 +534,61 @@ fn get_anki_addons21_path() -> Result {
.join("Anki2")
};
- Ok(anki_base_path.join("addons21"))
+ Ok(anki_base_path)
+}
+
+fn get_anki_addons21_path() -> Result {
+ Ok(get_anki_base_path()?.join("addons21"))
+}
+
+fn handle_uninstall(state: &State) -> Result {
+ println!("Uninstall Anki's program files? (y/n)");
+ print!("> ");
+ let _ = stdout().flush();
+
+ let mut input = String::new();
+ let _ = stdin().read_line(&mut input);
+ let input = input.trim().to_lowercase();
+
+ if input != "y" {
+ println!("Uninstall cancelled.");
+ println!();
+ return Ok(false);
+ }
+
+ // Remove program files
+ if state.uv_install_root.exists() {
+ anki_io::remove_dir_all(&state.uv_install_root)?;
+ println!("Program files removed.");
+ }
+
+ println!();
+ println!("Remove all profiles/cards? (y/n)");
+ print!("> ");
+ let _ = stdout().flush();
+
+ let mut input = String::new();
+ let _ = stdin().read_line(&mut input);
+ let input = input.trim().to_lowercase();
+
+ if input == "y" && state.anki_base_folder.exists() {
+ anki_io::remove_dir_all(&state.anki_base_folder)?;
+ println!("User data removed.");
+ }
+
+ println!();
+
+ // Platform-specific messages
+ #[cfg(target_os = "macos")]
+ platform::mac::finalize_uninstall();
+
+ #[cfg(target_os = "windows")]
+ platform::windows::finalize_uninstall();
+
+ #[cfg(all(unix, not(target_os = "macos")))]
+ platform::unix::finalize_uninstall();
+
+ Ok(true)
}
fn build_python_command(uv_install_root: &std::path::Path, args: &[String]) -> Result {
diff --git a/qt/launcher/src/platform/mac.rs b/qt/launcher/src/platform/mac.rs
index 292e48726..f97d7fd07 100644
--- a/qt/launcher/src/platform/mac.rs
+++ b/qt/launcher/src/platform/mac.rs
@@ -67,3 +67,32 @@ pub fn relaunch_in_terminal() -> Result<()> {
.ensure_spawn()?;
std::process::exit(0);
}
+
+pub fn finalize_uninstall() {
+ if let Ok(exe_path) = std::env::current_exe() {
+ // Find the .app bundle by walking up the directory tree
+ let mut app_bundle_path = exe_path.as_path();
+ while let Some(parent) = app_bundle_path.parent() {
+ if let Some(name) = parent.file_name() {
+ if name.to_string_lossy().ends_with(".app") {
+ let result = Command::new("trash").arg(parent).output();
+
+ match result {
+ Ok(output) if output.status.success() => {
+ println!("Anki has been uninstalled.");
+ return;
+ }
+ _ => {
+ // Fall back to manual instructions
+ println!(
+ "Please manually drag Anki.app to the trash to complete uninstall."
+ );
+ }
+ }
+ return;
+ }
+ }
+ app_bundle_path = parent;
+ }
+ }
+}
diff --git a/qt/launcher/src/platform/mod.rs b/qt/launcher/src/platform/mod.rs
index c94f1d1ac..235058757 100644
--- a/qt/launcher/src/platform/mod.rs
+++ b/qt/launcher/src/platform/mod.rs
@@ -2,7 +2,7 @@
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
#[cfg(all(unix, not(target_os = "macos")))]
-mod unix;
+pub mod unix;
#[cfg(target_os = "macos")]
pub mod mac;
diff --git a/qt/launcher/src/platform/unix.rs b/qt/launcher/src/platform/unix.rs
index 0430bfa96..f37ec81eb 100644
--- a/qt/launcher/src/platform/unix.rs
+++ b/qt/launcher/src/platform/unix.rs
@@ -47,3 +47,21 @@ pub fn relaunch_in_terminal() -> Result<()> {
// If no terminal worked, continue without relaunching
Ok(())
}
+
+pub fn finalize_uninstall() {
+ use std::io::stdin;
+ use std::io::stdout;
+ use std::io::Write;
+
+ let uninstall_script = std::path::Path::new("/usr/local/share/anki/uninstall.sh");
+
+ if uninstall_script.exists() {
+ println!("To finish uninstalling, run 'sudo /usr/local/share/anki/uninstall.sh'");
+ } else {
+ println!("Anki has been uninstalled.");
+ }
+ println!("Press enter to quit.");
+ let _ = stdout().flush();
+ let mut input = String::new();
+ let _ = stdin().read_line(&mut input);
+}
diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs
index 0a701c07a..ae22b8907 100644
--- a/qt/launcher/src/platform/windows.rs
+++ b/qt/launcher/src/platform/windows.rs
@@ -1,11 +1,17 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
+use std::ffi::OsStr;
+use std::io::stdin;
+use std::os::windows::ffi::OsStrExt;
use std::process::Command;
use anyhow::Context;
use anyhow::Result;
+use winapi::shared::minwindef::HKEY;
use winapi::um::wincon;
+use winapi::um::winnt::KEY_READ;
+use winapi::um::winreg;
pub fn ensure_terminal_shown() -> Result<()> {
unsafe {
@@ -79,3 +85,122 @@ fn reconnect_stdio_to_console() {
libc::freopen(conout.as_ptr(), w.as_ptr(), stderr());
}
}
+
+pub fn finalize_uninstall() {
+ let uninstaller_path = get_uninstaller_path();
+
+ match uninstaller_path {
+ Some(path) => {
+ println!("Launching Windows uninstaller...");
+ let result = Command::new(&path).env("ANKI_LAUNCHER", "1").spawn();
+
+ match result {
+ Ok(_) => {
+ println!("Uninstaller launched successfully.");
+ return;
+ }
+ Err(e) => {
+ println!("Failed to launch uninstaller: {}", e);
+ println!("You can manually run: {}", path.display());
+ }
+ }
+ }
+ None => {
+ println!("Windows uninstaller not found.");
+ println!("You may need to uninstall via Windows Settings > Apps.");
+ }
+ }
+ println!("Press enter to close...");
+ let mut input = String::new();
+ let _ = stdin().read_line(&mut input);
+}
+
+fn get_uninstaller_path() -> Option {
+ // Try to read install directory from registry
+ if let Some(install_dir) = read_registry_install_dir() {
+ let uninstaller = install_dir.join("uninstall.exe");
+ if uninstaller.exists() {
+ return Some(uninstaller);
+ }
+ }
+
+ // Fall back to default location
+ let default_dir = dirs::data_local_dir()?.join("Programs").join("Anki");
+ let uninstaller = default_dir.join("uninstall.exe");
+ if uninstaller.exists() {
+ return Some(uninstaller);
+ }
+
+ None
+}
+
+fn read_registry_install_dir() -> Option {
+ unsafe {
+ let mut hkey: HKEY = std::ptr::null_mut();
+
+ // Convert the registry path to wide string
+ let subkey: Vec = OsStr::new("SOFTWARE\\Anki")
+ .encode_wide()
+ .chain(std::iter::once(0))
+ .collect();
+
+ // Open the registry key
+ let result = winreg::RegOpenKeyExW(
+ winreg::HKEY_CURRENT_USER,
+ subkey.as_ptr(),
+ 0,
+ KEY_READ,
+ &mut hkey,
+ );
+
+ if result != 0 {
+ return None;
+ }
+
+ // Query the Install_Dir64 value
+ let value_name: Vec = OsStr::new("Install_Dir64")
+ .encode_wide()
+ .chain(std::iter::once(0))
+ .collect();
+
+ let mut value_type = 0u32;
+ let mut data_size = 0u32;
+
+ // First call to get the size
+ let result = winreg::RegQueryValueExW(
+ hkey,
+ value_name.as_ptr(),
+ std::ptr::null_mut(),
+ &mut value_type,
+ std::ptr::null_mut(),
+ &mut data_size,
+ );
+
+ if result != 0 || data_size == 0 {
+ winreg::RegCloseKey(hkey);
+ return None;
+ }
+
+ // Allocate buffer and read the value
+ let mut buffer: Vec = vec![0; (data_size / 2) as usize];
+ let result = winreg::RegQueryValueExW(
+ hkey,
+ value_name.as_ptr(),
+ std::ptr::null_mut(),
+ &mut value_type,
+ buffer.as_mut_ptr() as *mut u8,
+ &mut data_size,
+ );
+
+ winreg::RegCloseKey(hkey);
+
+ if result == 0 {
+ // Convert wide string back to PathBuf
+ let len = buffer.iter().position(|&x| x == 0).unwrap_or(buffer.len());
+ let path_str = String::from_utf16_lossy(&buffer[..len]);
+ Some(std::path::PathBuf::from(path_str))
+ } else {
+ None
+ }
+ }
+}
diff --git a/qt/launcher/win/anki.template.nsi b/qt/launcher/win/anki.template.nsi
index 7b2bfd8fc..84dedf9c8 100644
--- a/qt/launcher/win/anki.template.nsi
+++ b/qt/launcher/win/anki.template.nsi
@@ -250,8 +250,18 @@ FunctionEnd
; Uninstaller
function un.onInit
- MessageBox MB_OKCANCEL "This will remove Anki's program files, but will not delete your card data. If you wish to delete your card data as well, you can do so via File>Switch Profile inside Anki first. Are you sure you wish to uninstall Anki?" /SD IDOK IDOK next
- Quit
+ ; Check for ANKI_LAUNCHER environment variable
+ ReadEnvStr $R0 "ANKI_LAUNCHER"
+ ${If} $R0 != ""
+ ; Wait for launcher to exit
+ Sleep 2000
+ Goto next
+ ${Else}
+ ; Try to launch anki.exe with ANKI_LAUNCHER_UNINSTALL=1
+ IfFileExists "$INSTDIR\anki.exe" 0 next
+ nsExec::Exec 'cmd /c "set ANKI_LAUNCHER_UNINSTALL=1 && start /b "" "$INSTDIR\anki.exe""'
+ Quit
+ ${EndIf}
next:
functionEnd
From bdb3c714dcff64abba865b23ec2b5453f9ba659b Mon Sep 17 00:00:00 2001
From: Damien Elmes
Date: Sun, 29 Jun 2025 13:42:15 +0700
Subject: [PATCH 8/9] Put Python install inside uv root as well
---
qt/launcher/src/main.rs | 3 +++
1 file changed, 3 insertions(+)
diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs
index fdfa47e9e..64c0a5776 100644
--- a/qt/launcher/src/main.rs
+++ b/qt/launcher/src/main.rs
@@ -39,6 +39,7 @@ struct State {
no_cache_marker: std::path::PathBuf,
anki_base_folder: std::path::PathBuf,
uv_path: std::path::PathBuf,
+ uv_python_install_dir: std::path::PathBuf,
user_pyproject_path: std::path::PathBuf,
user_python_version_path: std::path::PathBuf,
dist_pyproject_path: std::path::PathBuf,
@@ -90,6 +91,7 @@ fn run() -> Result<()> {
no_cache_marker: uv_install_root.join("nocache"),
anki_base_folder: get_anki_base_path()?,
uv_path: exe_dir.join(get_uv_binary_name()),
+ uv_python_install_dir: uv_install_root.join("python"),
user_pyproject_path: uv_install_root.join("pyproject.toml"),
user_python_version_path: uv_install_root.join(".python-version"),
dist_pyproject_path: resources_dir.join("pyproject.toml"),
@@ -234,6 +236,7 @@ fn main_menu_loop(state: &State) -> Result<()> {
command
.current_dir(&state.uv_install_root)
.env("UV_CACHE_DIR", &state.uv_cache_dir)
+ .env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir)
.args(["sync", "--upgrade", "--managed-python"]);
// Add python version if .python-version file exists
From b8963b463ecc6ae721ef29592ac4b739d5d402fa Mon Sep 17 00:00:00 2001
From: Damien Elmes
Date: Sun, 29 Jun 2025 13:54:17 +0700
Subject: [PATCH 9/9] Fix Windows CI and minor display tweak
---
qt/launcher/src/main.rs | 1 +
qt/launcher/src/platform/windows.rs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/qt/launcher/src/main.rs b/qt/launcher/src/main.rs
index 64c0a5776..396b7b0ba 100644
--- a/qt/launcher/src/main.rs
+++ b/qt/launcher/src/main.rs
@@ -160,6 +160,7 @@ fn run() -> Result<()> {
} else {
// on Windows/macOS, the user needs to close the terminal/console
// currently, but ideas on how we can avoid this would be good!
+ println!();
println!("Anki will start shortly.");
println!("\x1B[1mYou can close this window.\x1B[0m\n");
}
diff --git a/qt/launcher/src/platform/windows.rs b/qt/launcher/src/platform/windows.rs
index ae22b8907..8a64de9c2 100644
--- a/qt/launcher/src/platform/windows.rs
+++ b/qt/launcher/src/platform/windows.rs
@@ -100,7 +100,7 @@ pub fn finalize_uninstall() {
return;
}
Err(e) => {
- println!("Failed to launch uninstaller: {}", e);
+ println!("Failed to launch uninstaller: {e}");
println!("You can manually run: {}", path.display());
}
}