Code drill feature parity, downloading snippets from github

Phase 1 and 2. Phase 3 will allow custom github repo input.
This commit is contained in:
2026-02-18 05:12:01 +00:00
parent 2d63cffb33
commit d0605f8426
11 changed files with 4520 additions and 372 deletions

View File

@@ -26,6 +26,14 @@ pub struct Config {
pub passage_paragraphs_per_book: usize,
#[serde(default = "default_passage_onboarding_done")]
pub passage_onboarding_done: bool,
#[serde(default = "default_code_downloads_enabled")]
pub code_downloads_enabled: bool,
#[serde(default = "default_code_download_dir")]
pub code_download_dir: String,
#[serde(default = "default_code_snippets_per_repo")]
pub code_snippets_per_repo: usize,
#[serde(default = "default_code_onboarding_done")]
pub code_onboarding_done: bool,
}
fn default_target_wpm() -> u32 {
@@ -63,6 +71,23 @@ fn default_passage_paragraphs_per_book() -> usize {
fn default_passage_onboarding_done() -> bool {
false
}
fn default_code_downloads_enabled() -> bool {
false
}
fn default_code_download_dir() -> String {
dirs::data_dir()
.unwrap_or_else(|| PathBuf::from("."))
.join("keydr")
.join("code")
.to_string_lossy()
.to_string()
}
fn default_code_snippets_per_repo() -> usize {
200
}
fn default_code_onboarding_done() -> bool {
false
}
impl Default for Config {
fn default() -> Self {
@@ -77,6 +102,10 @@ impl Default for Config {
passage_download_dir: default_passage_download_dir(),
passage_paragraphs_per_book: default_passage_paragraphs_per_book(),
passage_onboarding_done: default_passage_onboarding_done(),
code_downloads_enabled: default_code_downloads_enabled(),
code_download_dir: default_code_download_dir(),
code_snippets_per_repo: default_code_snippets_per_repo(),
code_onboarding_done: default_code_onboarding_done(),
}
}
}
@@ -114,4 +143,97 @@ impl Config {
pub fn target_cpm(&self) -> f64 {
self.target_wpm as f64 * 5.0
}
/// Validate `code_language` against known options, resetting to default if invalid.
/// Call after deserialization to handle stale/renamed keys from old configs.
pub fn normalize_code_language(&mut self, valid_keys: &[&str]) {
// Backwards compatibility: old "shell" key is now "bash".
if self.code_language == "shell" {
self.code_language = "bash".to_string();
}
if !valid_keys.contains(&self.code_language.as_str()) {
self.code_language = default_code_language();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_serde_defaults_from_empty() {
// Simulates loading an old config file with no code drill fields
let config: Config = toml::from_str("").unwrap();
assert_eq!(config.code_downloads_enabled, false);
assert_eq!(config.code_snippets_per_repo, 200);
assert_eq!(config.code_onboarding_done, false);
assert!(!config.code_download_dir.is_empty());
assert!(config.code_download_dir.contains("code"));
}
#[test]
fn test_config_serde_defaults_from_old_fields_only() {
// Simulates a config file that only has pre-existing fields
let toml_str = r#"
target_wpm = 60
theme = "monokai"
code_language = "go"
"#;
let config: Config = toml::from_str(toml_str).unwrap();
assert_eq!(config.target_wpm, 60);
assert_eq!(config.theme, "monokai");
assert_eq!(config.code_language, "go");
// New fields should have defaults
assert_eq!(config.code_downloads_enabled, false);
assert_eq!(config.code_snippets_per_repo, 200);
assert_eq!(config.code_onboarding_done, false);
}
#[test]
fn test_config_serde_roundtrip() {
let config = Config::default();
let serialized = toml::to_string_pretty(&config).unwrap();
let deserialized: Config = toml::from_str(&serialized).unwrap();
assert_eq!(config.code_downloads_enabled, deserialized.code_downloads_enabled);
assert_eq!(config.code_download_dir, deserialized.code_download_dir);
assert_eq!(config.code_snippets_per_repo, deserialized.code_snippets_per_repo);
assert_eq!(config.code_onboarding_done, deserialized.code_onboarding_done);
}
#[test]
fn test_normalize_code_language_valid_key_unchanged() {
let mut config = Config::default();
config.code_language = "python".to_string();
let valid_keys = vec!["rust", "python", "javascript", "go", "all"];
config.normalize_code_language(&valid_keys);
assert_eq!(config.code_language, "python");
}
#[test]
fn test_normalize_code_language_invalid_key_resets() {
let mut config = Config::default();
config.code_language = "haskell".to_string();
let valid_keys = vec!["rust", "python", "javascript", "go", "all"];
config.normalize_code_language(&valid_keys);
assert_eq!(config.code_language, "rust");
}
#[test]
fn test_normalize_code_language_empty_string_resets() {
let mut config = Config::default();
config.code_language = String::new();
let valid_keys = vec!["rust", "python", "javascript", "go", "all"];
config.normalize_code_language(&valid_keys);
assert_eq!(config.code_language, "rust");
}
#[test]
fn test_normalize_code_language_shell_maps_to_bash() {
let mut config = Config::default();
config.code_language = "shell".to_string();
let valid_keys = vec!["rust", "python", "javascript", "go", "bash", "all"];
config.normalize_code_language(&valid_keys);
assert_eq!(config.code_language, "bash");
}
}