mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
Merge branch 'main' into editor-3830
This commit is contained in:
commit
543f97eb10
15 changed files with 1606 additions and 1324 deletions
2
.version
2
.version
|
@ -1 +1 @@
|
||||||
25.07.3
|
25.07.5
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit b90ef6f03c251eb336029ac7c5f551200d41273f
|
Subproject commit 939298f7c461407951988f362b1a08b451336a1e
|
|
@ -1 +1 @@
|
||||||
Subproject commit 9aa63c335c61b30421d39cf43fd8e3975179059c
|
Subproject commit bc2da83c77749d96f3df8144f00c87d68dd2187a
|
|
@ -133,6 +133,7 @@ class Card(DeprecatedNamesMixin):
|
||||||
memory_state=self.memory_state,
|
memory_state=self.memory_state,
|
||||||
desired_retention=self.desired_retention,
|
desired_retention=self.desired_retention,
|
||||||
decay=self.decay,
|
decay=self.decay,
|
||||||
|
last_review_time_secs=self.last_review_time,
|
||||||
)
|
)
|
||||||
|
|
||||||
@deprecated(info="please use col.update_card()")
|
@deprecated(info="please use col.update_card()")
|
||||||
|
|
|
@ -51,6 +51,7 @@ class CardInfoDialog(QDialog):
|
||||||
|
|
||||||
def _setup_ui(self, card_id: CardId | None) -> None:
|
def _setup_ui(self, card_id: CardId | None) -> None:
|
||||||
self.mw.garbage_collect_on_dialog_finish(self)
|
self.mw.garbage_collect_on_dialog_finish(self)
|
||||||
|
self.setMinimumSize(400, 300)
|
||||||
disable_help_button(self)
|
disable_help_button(self)
|
||||||
restoreGeom(self, self.GEOMETRY_KEY, default_size=(800, 800))
|
restoreGeom(self, self.GEOMETRY_KEY, default_size=(800, 800))
|
||||||
add_close_shortcut(self)
|
add_close_shortcut(self)
|
||||||
|
|
|
@ -633,7 +633,7 @@ class QtAudioInputRecorder(Recorder):
|
||||||
from PyQt6.QtMultimedia import QAudioFormat, QAudioSource # type: ignore
|
from PyQt6.QtMultimedia import QAudioFormat, QAudioSource # type: ignore
|
||||||
|
|
||||||
format = QAudioFormat()
|
format = QAudioFormat()
|
||||||
format.setChannelCount(1)
|
format.setChannelCount(2)
|
||||||
format.setSampleRate(44100)
|
format.setSampleRate(44100)
|
||||||
format.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
format.setSampleFormat(QAudioFormat.SampleFormat.Int16)
|
||||||
|
|
||||||
|
|
|
@ -38,19 +38,19 @@ cp ../pyproject.toml "$APP_LAUNCHER/Contents/Resources/"
|
||||||
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"
|
cp ../../../.python-version "$APP_LAUNCHER/Contents/Resources/"
|
||||||
cp ../versions.py "$APP_LAUNCHER/Contents/Resources/"
|
cp ../versions.py "$APP_LAUNCHER/Contents/Resources/"
|
||||||
|
|
||||||
# Codesign
|
# Codesign/bundle
|
||||||
for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do
|
|
||||||
codesign --force -vvvv -o runtime -s "Developer ID Application:" \
|
|
||||||
--entitlements entitlements.python.xml \
|
|
||||||
"$i"
|
|
||||||
done
|
|
||||||
|
|
||||||
# Check
|
|
||||||
codesign -vvv "$APP_LAUNCHER"
|
|
||||||
spctl -a "$APP_LAUNCHER"
|
|
||||||
|
|
||||||
# Notarize and bundle (skip if NODMG is set)
|
|
||||||
if [ -z "$NODMG" ]; then
|
if [ -z "$NODMG" ]; then
|
||||||
|
for i in "$APP_LAUNCHER/Contents/MacOS/uv" "$APP_LAUNCHER/Contents/MacOS/launcher" "$APP_LAUNCHER"; do
|
||||||
|
codesign --force -vvvv -o runtime -s "Developer ID Application:" \
|
||||||
|
--entitlements entitlements.python.xml \
|
||||||
|
"$i"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check
|
||||||
|
codesign -vvv "$APP_LAUNCHER"
|
||||||
|
spctl -a "$APP_LAUNCHER"
|
||||||
|
|
||||||
|
# Notarize and build dmg
|
||||||
./notarize.sh "$OUTPUT_DIR"
|
./notarize.sh "$OUTPUT_DIR"
|
||||||
./dmg/build.sh "$OUTPUT_DIR"
|
./dmg/build.sh "$OUTPUT_DIR"
|
||||||
fi
|
fi
|
|
@ -11,7 +11,6 @@ use std::time::SystemTime;
|
||||||
use std::time::UNIX_EPOCH;
|
use std::time::UNIX_EPOCH;
|
||||||
|
|
||||||
use anki_io::copy_file;
|
use anki_io::copy_file;
|
||||||
use anki_io::copy_if_newer;
|
|
||||||
use anki_io::create_dir_all;
|
use anki_io::create_dir_all;
|
||||||
use anki_io::modified_time;
|
use anki_io::modified_time;
|
||||||
use anki_io::read_file;
|
use anki_io::read_file;
|
||||||
|
@ -50,6 +49,7 @@ struct State {
|
||||||
pyproject_modified_by_user: bool,
|
pyproject_modified_by_user: bool,
|
||||||
previous_version: Option<String>,
|
previous_version: Option<String>,
|
||||||
resources_dir: std::path::PathBuf,
|
resources_dir: std::path::PathBuf,
|
||||||
|
venv_folder: std::path::PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
@ -111,6 +111,7 @@ fn run() -> Result<()> {
|
||||||
pyproject_modified_by_user: false, // calculated later
|
pyproject_modified_by_user: false, // calculated later
|
||||||
previous_version: None,
|
previous_version: None,
|
||||||
resources_dir,
|
resources_dir,
|
||||||
|
venv_folder: uv_install_root.join(".venv"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for uninstall request from Windows uninstaller
|
// Check for uninstall request from Windows uninstaller
|
||||||
|
@ -120,15 +121,11 @@ fn run() -> Result<()> {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create install directory and copy project files in
|
// Create install directory
|
||||||
create_dir_all(&state.uv_install_root)?;
|
create_dir_all(&state.uv_install_root)?;
|
||||||
copy_if_newer(&state.dist_pyproject_path, &state.user_pyproject_path)?;
|
|
||||||
copy_if_newer(
|
|
||||||
&state.dist_python_version_path,
|
|
||||||
&state.user_python_version_path,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let launcher_requested = state.launcher_trigger_file.exists();
|
let launcher_requested =
|
||||||
|
state.launcher_trigger_file.exists() || !state.user_pyproject_path.exists();
|
||||||
|
|
||||||
// Calculate whether user has custom edits that need syncing
|
// Calculate whether user has custom edits that need syncing
|
||||||
let pyproject_time = file_timestamp_secs(&state.user_pyproject_path);
|
let pyproject_time = file_timestamp_secs(&state.user_pyproject_path);
|
||||||
|
@ -158,7 +155,7 @@ fn run() -> Result<()> {
|
||||||
|
|
||||||
check_versions(&mut state);
|
check_versions(&mut state);
|
||||||
|
|
||||||
let first_run = !state.uv_install_root.join(".venv").exists();
|
let first_run = !state.venv_folder.exists();
|
||||||
if first_run {
|
if first_run {
|
||||||
handle_version_install_or_update(&state, MainMenuChoice::Latest)?;
|
handle_version_install_or_update(&state, MainMenuChoice::Latest)?;
|
||||||
} else {
|
} else {
|
||||||
|
@ -166,7 +163,7 @@ fn run() -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write marker file to indicate we've completed the sync process
|
// Write marker file to indicate we've completed the sync process
|
||||||
write_sync_marker(&state.sync_complete_marker)?;
|
write_sync_marker(&state)?;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
|
@ -192,13 +189,15 @@ fn run() -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_aqt_version(
|
fn extract_aqt_version(state: &State) -> Option<String> {
|
||||||
uv_path: &std::path::Path,
|
// Check if .venv exists first
|
||||||
uv_install_root: &std::path::Path,
|
if !state.venv_folder.exists() {
|
||||||
) -> Option<String> {
|
return None;
|
||||||
let output = Command::new(uv_path)
|
}
|
||||||
.current_dir(uv_install_root)
|
|
||||||
.env("VIRTUAL_ENV", uv_install_root.join(".venv"))
|
let output = Command::new(&state.uv_path)
|
||||||
|
.current_dir(&state.uv_install_root)
|
||||||
|
.env("VIRTUAL_ENV", &state.venv_folder)
|
||||||
.args(["pip", "show", "aqt"])
|
.args(["pip", "show", "aqt"])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
@ -223,7 +222,7 @@ fn check_versions(state: &mut State) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine current version by invoking uv pip show aqt
|
// Determine current version by invoking uv pip show aqt
|
||||||
match extract_aqt_version(&state.uv_path, &state.uv_install_root) {
|
match extract_aqt_version(state) {
|
||||||
Some(version) => {
|
Some(version) => {
|
||||||
state.current_version = Some(version);
|
state.current_version = Some(version);
|
||||||
}
|
}
|
||||||
|
@ -248,12 +247,12 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
|
||||||
update_pyproject_for_version(choice.clone(), state)?;
|
update_pyproject_for_version(choice.clone(), state)?;
|
||||||
|
|
||||||
// Extract current version before syncing (but don't write to file yet)
|
// Extract current version before syncing (but don't write to file yet)
|
||||||
let previous_version_to_save = extract_aqt_version(&state.uv_path, &state.uv_install_root);
|
let previous_version_to_save = extract_aqt_version(state);
|
||||||
|
|
||||||
// Remove sync marker before attempting sync
|
// Remove sync marker before attempting sync
|
||||||
let _ = remove_file(&state.sync_complete_marker);
|
let _ = remove_file(&state.sync_complete_marker);
|
||||||
|
|
||||||
println!("\x1B[1mUpdating Anki...\x1B[0m\n");
|
println!("Updating Anki...\n");
|
||||||
|
|
||||||
let python_version_trimmed = if state.user_python_version_path.exists() {
|
let python_version_trimmed = if state.user_python_version_path.exists() {
|
||||||
let python_version = read_file(&state.user_python_version_path)?;
|
let python_version = read_file(&state.user_python_version_path)?;
|
||||||
|
@ -264,6 +263,11 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let have_venv = state.venv_folder.exists();
|
||||||
|
if cfg!(target_os = "macos") && !have_developer_tools() && !have_venv {
|
||||||
|
println!("If you see a pop-up about 'install_name_tool', you can cancel it, and ignore the warning below.\n");
|
||||||
|
}
|
||||||
|
|
||||||
// Prepare to sync the venv
|
// Prepare to sync the venv
|
||||||
let mut command = Command::new(&state.uv_path);
|
let mut command = Command::new(&state.uv_path);
|
||||||
command.current_dir(&state.uv_install_root);
|
command.current_dir(&state.uv_install_root);
|
||||||
|
@ -275,9 +279,27 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// remove CONDA_PREFIX/bin from PATH to avoid conda interference
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if let Ok(conda_prefix) = std::env::var("CONDA_PREFIX") {
|
||||||
|
if let Ok(current_path) = std::env::var("PATH") {
|
||||||
|
let conda_bin = format!("{conda_prefix}/bin");
|
||||||
|
let filtered_paths: Vec<&str> = current_path
|
||||||
|
.split(':')
|
||||||
|
.filter(|&path| path != conda_bin)
|
||||||
|
.collect();
|
||||||
|
let new_path = filtered_paths.join(":");
|
||||||
|
command.env("PATH", new_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
command
|
command
|
||||||
.env("UV_CACHE_DIR", &state.uv_cache_dir)
|
.env("UV_CACHE_DIR", &state.uv_cache_dir)
|
||||||
.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir)
|
.env("UV_PYTHON_INSTALL_DIR", &state.uv_python_install_dir)
|
||||||
|
.env(
|
||||||
|
"UV_HTTP_TIMEOUT",
|
||||||
|
std::env::var("UV_HTTP_TIMEOUT").unwrap_or_else(|_| "180".to_string()),
|
||||||
|
)
|
||||||
.args(["sync", "--upgrade", "--managed-python", "--no-config"]);
|
.args(["sync", "--upgrade", "--managed-python", "--no-config"]);
|
||||||
|
|
||||||
// Add python version if .python-version file exists
|
// Add python version if .python-version file exists
|
||||||
|
@ -293,7 +315,7 @@ fn handle_version_install_or_update(state: &State, choice: MainMenuChoice) -> Re
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
// Sync succeeded
|
// Sync succeeded
|
||||||
if matches!(&choice, MainMenuChoice::Version(VersionKind::PyOxidizer(_))) {
|
if matches!(&choice, MainMenuChoice::Version(VersionKind::PyOxidizer(_))) {
|
||||||
inject_helper_addon(&state.uv_install_root)?;
|
inject_helper_addon()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now that sync succeeded, save the previous version
|
// Now that sync succeeded, save the previous version
|
||||||
|
@ -364,9 +386,7 @@ fn main_menu_loop(state: &State) -> Result<()> {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
|
choice @ (MainMenuChoice::Latest | MainMenuChoice::Version(_)) => {
|
||||||
if handle_version_install_or_update(state, choice.clone()).is_err() {
|
handle_version_install_or_update(state, choice.clone())?;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -374,12 +394,12 @@ fn main_menu_loop(state: &State) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_sync_marker(sync_complete_marker: &std::path::Path) -> Result<()> {
|
fn write_sync_marker(state: &State) -> Result<()> {
|
||||||
let timestamp = SystemTime::now()
|
let timestamp = SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
.context("Failed to get system time")?
|
.context("Failed to get system time")?
|
||||||
.as_secs();
|
.as_secs();
|
||||||
write_file(sync_complete_marker, timestamp.to_string())?;
|
write_file(&state.sync_complete_marker, timestamp.to_string())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -473,8 +493,6 @@ fn get_main_menu_choice(state: &State) -> Result<MainMenuChoice> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
fn get_version_kind(state: &State) -> Result<Option<VersionKind>> {
|
||||||
println!("Please wait...");
|
|
||||||
|
|
||||||
let releases = get_releases(state)?;
|
let releases = get_releases(state)?;
|
||||||
let releases_str = releases
|
let releases_str = releases
|
||||||
.latest
|
.latest
|
||||||
|
@ -633,15 +651,32 @@ fn fetch_versions(state: &State) -> Result<Vec<String>> {
|
||||||
|
|
||||||
let mut cmd = Command::new(&state.uv_path);
|
let mut cmd = Command::new(&state.uv_path);
|
||||||
cmd.current_dir(&state.uv_install_root)
|
cmd.current_dir(&state.uv_install_root)
|
||||||
.args(["run", "--no-project"])
|
.args(["run", "--no-project", "--no-config", "--managed-python"])
|
||||||
.arg(&versions_script);
|
.args(["--with", "pip-system-certs"]);
|
||||||
|
|
||||||
let output = cmd.utf8_output()?;
|
let python_version = read_file(&state.dist_python_version_path)?;
|
||||||
|
let python_version_str =
|
||||||
|
String::from_utf8(python_version).context("Invalid UTF-8 in .python-version")?;
|
||||||
|
let version_trimmed = python_version_str.trim();
|
||||||
|
if !version_trimmed.is_empty() {
|
||||||
|
cmd.args(["--python", version_trimmed]);
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.arg(&versions_script);
|
||||||
|
|
||||||
|
let output = match cmd.utf8_output() {
|
||||||
|
Ok(output) => output,
|
||||||
|
Err(e) => {
|
||||||
|
print!("Unable to check for Anki versions. Please check your internet connection.\n\n");
|
||||||
|
return Err(e.into());
|
||||||
|
}
|
||||||
|
};
|
||||||
let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?;
|
let versions = serde_json::from_str(&output.stdout).context("Failed to parse versions JSON")?;
|
||||||
Ok(versions)
|
Ok(versions)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_releases(state: &State) -> Result<Releases> {
|
fn get_releases(state: &State) -> Result<Releases> {
|
||||||
|
println!("Checking for updates...");
|
||||||
let include_prereleases = state.prerelease_marker.exists();
|
let include_prereleases = state.prerelease_marker.exists();
|
||||||
let all_versions = fetch_versions(state)?;
|
let all_versions = fetch_versions(state)?;
|
||||||
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
let all_versions = filter_and_normalize_versions(all_versions, include_prereleases);
|
||||||
|
@ -768,7 +803,7 @@ fn parse_version_kind(version: &str) -> Option<VersionKind> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn inject_helper_addon(_uv_install_root: &std::path::Path) -> Result<()> {
|
fn inject_helper_addon() -> Result<()> {
|
||||||
let addons21_path = get_anki_addons21_path()?;
|
let addons21_path = get_anki_addons21_path()?;
|
||||||
|
|
||||||
if !addons21_path.exists() {
|
if !addons21_path.exists() {
|
||||||
|
@ -870,16 +905,24 @@ fn handle_uninstall(state: &State) -> Result<bool> {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn have_developer_tools() -> bool {
|
||||||
|
Command::new("xcode-select")
|
||||||
|
.args(["-p"])
|
||||||
|
.output()
|
||||||
|
.map(|output| output.status.success())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
|
fn build_python_command(state: &State, args: &[String]) -> Result<Command> {
|
||||||
let python_exe = if cfg!(target_os = "windows") {
|
let python_exe = if cfg!(target_os = "windows") {
|
||||||
let show_console = std::env::var("ANKI_CONSOLE").is_ok();
|
let show_console = std::env::var("ANKI_CONSOLE").is_ok();
|
||||||
if show_console {
|
if show_console {
|
||||||
state.uv_install_root.join(".venv/Scripts/python.exe")
|
state.venv_folder.join("Scripts/python.exe")
|
||||||
} else {
|
} else {
|
||||||
state.uv_install_root.join(".venv/Scripts/pythonw.exe")
|
state.venv_folder.join("Scripts/pythonw.exe")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
state.uv_install_root.join(".venv/bin/python")
|
state.venv_folder.join("bin/python")
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut cmd = Command::new(&python_exe);
|
let mut cmd = Command::new(&python_exe);
|
||||||
|
|
|
@ -5,6 +5,10 @@ import json
|
||||||
import sys
|
import sys
|
||||||
import urllib.request
|
import urllib.request
|
||||||
|
|
||||||
|
import pip_system_certs.wrapt_requests
|
||||||
|
|
||||||
|
pip_system_certs.wrapt_requests.inject_truststore()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Fetch and return all versions from PyPI, sorted by upload time."""
|
"""Fetch and return all versions from PyPI, sorted by upload time."""
|
||||||
|
|
|
@ -11,6 +11,24 @@ use snafu::ensure;
|
||||||
use snafu::ResultExt;
|
use snafu::ResultExt;
|
||||||
use snafu::Snafu;
|
use snafu::Snafu;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CodeDisplay(Option<i32>);
|
||||||
|
|
||||||
|
impl std::fmt::Display for CodeDisplay {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self.0 {
|
||||||
|
Some(code) => write!(f, "{code}"),
|
||||||
|
None => write!(f, "?"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Option<i32>> for CodeDisplay {
|
||||||
|
fn from(code: Option<i32>) -> Self {
|
||||||
|
CodeDisplay(code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Snafu)]
|
#[derive(Debug, Snafu)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[snafu(display("Failed to execute: {cmdline}"))]
|
#[snafu(display("Failed to execute: {cmdline}"))]
|
||||||
|
@ -18,8 +36,15 @@ pub enum Error {
|
||||||
cmdline: String,
|
cmdline: String,
|
||||||
source: std::io::Error,
|
source: std::io::Error,
|
||||||
},
|
},
|
||||||
#[snafu(display("Failed with code {code:?}: {cmdline}"))]
|
#[snafu(display("Failed to run ({code}): {cmdline}"))]
|
||||||
ReturnedError { cmdline: String, code: Option<i32> },
|
ReturnedError { cmdline: String, code: CodeDisplay },
|
||||||
|
#[snafu(display("Failed to run ({code}): {cmdline}: {stdout}{stderr}"))]
|
||||||
|
ReturnedWithOutputError {
|
||||||
|
cmdline: String,
|
||||||
|
code: CodeDisplay,
|
||||||
|
stdout: String,
|
||||||
|
stderr: String,
|
||||||
|
},
|
||||||
#[snafu(display("Couldn't decode stdout/stderr as utf8"))]
|
#[snafu(display("Couldn't decode stdout/stderr as utf8"))]
|
||||||
InvalidUtf8 {
|
InvalidUtf8 {
|
||||||
cmdline: String,
|
cmdline: String,
|
||||||
|
@ -71,31 +96,36 @@ impl CommandExt for Command {
|
||||||
status.success(),
|
status.success(),
|
||||||
ReturnedSnafu {
|
ReturnedSnafu {
|
||||||
cmdline: get_cmdline(self),
|
cmdline: get_cmdline(self),
|
||||||
code: status.code(),
|
code: CodeDisplay::from(status.code()),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn utf8_output(&mut self) -> Result<Utf8Output> {
|
fn utf8_output(&mut self) -> Result<Utf8Output> {
|
||||||
|
let cmdline = get_cmdline(self);
|
||||||
let output = self.output().with_context(|_| DidNotExecuteSnafu {
|
let output = self.output().with_context(|_| DidNotExecuteSnafu {
|
||||||
cmdline: get_cmdline(self),
|
cmdline: cmdline.clone(),
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
let stdout = String::from_utf8(output.stdout).with_context(|_| InvalidUtf8Snafu {
|
||||||
|
cmdline: cmdline.clone(),
|
||||||
|
})?;
|
||||||
|
let stderr = String::from_utf8(output.stderr).with_context(|_| InvalidUtf8Snafu {
|
||||||
|
cmdline: cmdline.clone(),
|
||||||
|
})?;
|
||||||
|
|
||||||
ensure!(
|
ensure!(
|
||||||
output.status.success(),
|
output.status.success(),
|
||||||
ReturnedSnafu {
|
ReturnedWithOutputSnafu {
|
||||||
cmdline: get_cmdline(self),
|
cmdline,
|
||||||
code: output.status.code(),
|
code: CodeDisplay::from(output.status.code()),
|
||||||
|
stdout: stdout.clone(),
|
||||||
|
stderr: stderr.clone(),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
Ok(Utf8Output {
|
|
||||||
stdout: String::from_utf8(output.stdout).with_context(|_| InvalidUtf8Snafu {
|
Ok(Utf8Output { stdout, stderr })
|
||||||
cmdline: get_cmdline(self),
|
|
||||||
})?,
|
|
||||||
stderr: String::from_utf8(output.stderr).with_context(|_| InvalidUtf8Snafu {
|
|
||||||
cmdline: get_cmdline(self),
|
|
||||||
})?,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_spawn(&mut self) -> Result<std::process::Child> {
|
fn ensure_spawn(&mut self) -> Result<std::process::Child> {
|
||||||
|
@ -135,7 +165,10 @@ mod test {
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
assert!(matches!(
|
assert!(matches!(
|
||||||
Command::new("false").ensure_success(),
|
Command::new("false").ensure_success(),
|
||||||
Err(Error::ReturnedError { code: Some(1), .. })
|
Err(Error::ReturnedError {
|
||||||
|
code: CodeDisplay(_),
|
||||||
|
..
|
||||||
|
})
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,14 +12,20 @@ impl Collection {
|
||||||
.map(component_to_regex)
|
.map(component_to_regex)
|
||||||
.collect::<Result<_, _>>()?;
|
.collect::<Result<_, _>>()?;
|
||||||
let mut tags = vec![];
|
let mut tags = vec![];
|
||||||
|
let mut priority = vec![];
|
||||||
self.storage.get_tags_by_predicate(|tag| {
|
self.storage.get_tags_by_predicate(|tag| {
|
||||||
if tags.len() <= limit && filters_match(&filters, tag) {
|
if priority.len() + tags.len() <= limit {
|
||||||
tags.push(tag.to_string());
|
match filters_match(&filters, tag) {
|
||||||
|
Some(true) => priority.push(tag.to_string()),
|
||||||
|
Some(_) => tags.push(tag.to_string()),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// we only need the tag name
|
// we only need the tag name
|
||||||
false
|
false
|
||||||
})?;
|
})?;
|
||||||
Ok(tags)
|
priority.append(&mut tags);
|
||||||
|
Ok(priority)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,20 +33,26 @@ fn component_to_regex(component: &str) -> Result<Regex> {
|
||||||
Regex::new(&format!("(?i){}", regex::escape(component))).map_err(Into::into)
|
Regex::new(&format!("(?i){}", regex::escape(component))).map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn filters_match(filters: &[Regex], tag: &str) -> bool {
|
/// Returns None if tag wasn't a match, otherwise whether it was a consecutive
|
||||||
|
/// prefix match
|
||||||
|
fn filters_match(filters: &[Regex], tag: &str) -> Option<bool> {
|
||||||
let mut remaining_tag_components = tag.split("::");
|
let mut remaining_tag_components = tag.split("::");
|
||||||
|
let mut is_prefix = true;
|
||||||
'outer: for filter in filters {
|
'outer: for filter in filters {
|
||||||
loop {
|
loop {
|
||||||
if let Some(component) = remaining_tag_components.next() {
|
if let Some(component) = remaining_tag_components.next() {
|
||||||
if filter.is_match(component) {
|
if let Some(m) = filter.find(component) {
|
||||||
|
is_prefix &= m.start() == 0;
|
||||||
continue 'outer;
|
continue 'outer;
|
||||||
|
} else {
|
||||||
|
is_prefix = false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
true
|
Some(is_prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -50,28 +62,32 @@ mod test {
|
||||||
#[test]
|
#[test]
|
||||||
fn matching() -> Result<()> {
|
fn matching() -> Result<()> {
|
||||||
let filters = &[component_to_regex("b")?];
|
let filters = &[component_to_regex("b")?];
|
||||||
assert!(filters_match(filters, "ABC"));
|
assert!(filters_match(filters, "ABC").is_some());
|
||||||
assert!(filters_match(filters, "ABC::def"));
|
assert!(filters_match(filters, "ABC::def").is_some());
|
||||||
assert!(filters_match(filters, "def::abc"));
|
assert!(filters_match(filters, "def::abc").is_some());
|
||||||
assert!(!filters_match(filters, "def"));
|
assert!(filters_match(filters, "def").is_none());
|
||||||
|
|
||||||
let filters = &[component_to_regex("b")?, component_to_regex("E")?];
|
let filters = &[component_to_regex("b")?, component_to_regex("E")?];
|
||||||
assert!(!filters_match(filters, "ABC"));
|
assert!(filters_match(filters, "ABC").is_none());
|
||||||
assert!(filters_match(filters, "ABC::def"));
|
assert!(filters_match(filters, "ABC::def").is_some());
|
||||||
assert!(!filters_match(filters, "def::abc"));
|
assert!(filters_match(filters, "def::abc").is_none());
|
||||||
assert!(!filters_match(filters, "def"));
|
assert!(filters_match(filters, "def").is_none());
|
||||||
|
|
||||||
let filters = &[
|
let filters = &[
|
||||||
component_to_regex("a")?,
|
component_to_regex("a")?,
|
||||||
component_to_regex("c")?,
|
component_to_regex("c")?,
|
||||||
component_to_regex("e")?,
|
component_to_regex("e")?,
|
||||||
];
|
];
|
||||||
assert!(!filters_match(filters, "ace"));
|
assert!(filters_match(filters, "ace").is_none());
|
||||||
assert!(!filters_match(filters, "a::c"));
|
assert!(filters_match(filters, "a::c").is_none());
|
||||||
assert!(!filters_match(filters, "c::e"));
|
assert!(filters_match(filters, "c::e").is_none());
|
||||||
assert!(filters_match(filters, "a::c::e"));
|
assert!(filters_match(filters, "a::c::e").is_some());
|
||||||
assert!(filters_match(filters, "a::b::c::d::e"));
|
assert!(filters_match(filters, "a::b::c::d::e").is_some());
|
||||||
assert!(filters_match(filters, "1::a::b::c::d::e::f"));
|
assert!(filters_match(filters, "1::a::b::c::d::e::f").is_some());
|
||||||
|
|
||||||
|
assert_eq!(filters_match(filters, "a1::c2::e3"), Some(true));
|
||||||
|
assert_eq!(filters_match(filters, "a1::c2::?::e4"), Some(false));
|
||||||
|
assert_eq!(filters_match(filters, "a1::c2::3e"), Some(false));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -24,6 +24,7 @@ export const HelpPage = {
|
||||||
displayOrder: "https://docs.ankiweb.net/deck-options.html#display-order",
|
displayOrder: "https://docs.ankiweb.net/deck-options.html#display-order",
|
||||||
maximumReviewsday: "https://docs.ankiweb.net/deck-options.html#maximum-reviewsday",
|
maximumReviewsday: "https://docs.ankiweb.net/deck-options.html#maximum-reviewsday",
|
||||||
newCardsday: "https://docs.ankiweb.net/deck-options.html#new-cardsday",
|
newCardsday: "https://docs.ankiweb.net/deck-options.html#new-cardsday",
|
||||||
|
limitsFromTop: "https://docs.ankiweb.net/deck-options.html#limits-start-from-top",
|
||||||
dailyLimits: "https://docs.ankiweb.net/deck-options.html#daily-limits",
|
dailyLimits: "https://docs.ankiweb.net/deck-options.html#daily-limits",
|
||||||
audio: "https://docs.ankiweb.net/deck-options.html#audio",
|
audio: "https://docs.ankiweb.net/deck-options.html#audio",
|
||||||
fsrs: "http://docs.ankiweb.net/deck-options.html#fsrs",
|
fsrs: "http://docs.ankiweb.net/deck-options.html#fsrs",
|
||||||
|
|
|
@ -140,7 +140,7 @@
|
||||||
applyAllParentLimits: {
|
applyAllParentLimits: {
|
||||||
title: tr.deckConfigApplyAllParentLimits(),
|
title: tr.deckConfigApplyAllParentLimits(),
|
||||||
help: applyAllParentLimitsHelp,
|
help: applyAllParentLimitsHelp,
|
||||||
url: HelpPage.DeckOptions.newCardsday,
|
url: HelpPage.DeckOptions.limitsFromTop,
|
||||||
global: true,
|
global: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -106,6 +106,9 @@ function initCanvas(): fabric.Canvas {
|
||||||
fabric.Object.prototype.cornerStyle = "circle";
|
fabric.Object.prototype.cornerStyle = "circle";
|
||||||
fabric.Object.prototype.cornerStrokeColor = "#000000";
|
fabric.Object.prototype.cornerStrokeColor = "#000000";
|
||||||
fabric.Object.prototype.padding = 8;
|
fabric.Object.prototype.padding = 8;
|
||||||
|
// snap rotation around 0 by +-3deg
|
||||||
|
fabric.Object.prototype.snapAngle = 360;
|
||||||
|
fabric.Object.prototype.snapThreshold = 3;
|
||||||
// disable rotation when selecting
|
// disable rotation when selecting
|
||||||
canvas.on("selection:created", () => {
|
canvas.on("selection:created", () => {
|
||||||
const g = canvas.getActiveObject();
|
const g = canvas.getActiveObject();
|
||||||
|
|
Loading…
Reference in a new issue