aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-05-30 13:07:16 -0500
committerCraig Jennings <c@cjennings.net>2026-05-30 13:07:16 -0500
commit038d59b7e548d2323f43dcd92ba14cba876d840d (patch)
tree34f468689b3e4549918f1307d83ad8fda4eea7fa
parent23d87c187c950b047fd44ecb9c3d825b37712fc0 (diff)
downloadrulesets-038d59b7e548d2323f43dcd92ba14cba876d840d.tar.gz
rulesets-038d59b7e548d2323f43dcd92ba14cba876d840d.zip
feat(drill-to-anki): default to phone sync dir and basename deck name
Two default-behavior tweaks from real use. Output now defaults to ~/sync/phone/anki/ instead of ~/sync/org/drill/. The .apkg is a mobile-Anki artifact the phone picks up from its sync dir, while the org source stays in the project. Deck name now defaults to the raw input basename, case preserved. That drops the #+TITLE preference, which leaked tool-name jargon like "DeepSat org-drill flashcards" into the Anki deck list. The --deck and --output flags still override both. I dropped the now-unused title_from_org helper and added a test covering the two defaults. genanki is stubbed in the test since uv resolves it only at runtime.
-rwxr-xr-x.ai/scripts/drill-to-anki.py38
-rw-r--r--.ai/scripts/tests/test_drill_to_anki.py44
-rwxr-xr-xclaude-templates/.ai/scripts/drill-to-anki.py38
-rw-r--r--claude-templates/.ai/scripts/tests/test_drill_to_anki.py44
4 files changed, 114 insertions, 50 deletions
diff --git a/.ai/scripts/drill-to-anki.py b/.ai/scripts/drill-to-anki.py
index 543ccd8..1050021 100755
--- a/.ai/scripts/drill-to-anki.py
+++ b/.ai/scripts/drill-to-anki.py
@@ -13,15 +13,13 @@ Parses org-drill structure:
text (sans :drill: tag). Back = entry body with newlines converted
to <br>.
-Deck name defaults to the org file's #+TITLE if present, otherwise the
-input basename. Deck and model IDs are derived from the deck name via
-stable hash so re-importing the same deck updates existing cards
-instead of duplicating them.
+Deck name defaults to the input basename, case preserved. Deck and model
+IDs are derived from the deck name via stable hash so re-importing the
+same deck updates existing cards instead of duplicating them.
-Output defaults to ~/sync/org/drill/<input-basename>.apkg (matches
-Craig's project convention where org source lives in the project and
-symlinks into ~/sync/org/drill/, with the .apkg writing directly into
-~/sync/org/drill/ as a build artifact).
+Output defaults to ~/sync/phone/anki/<input-basename>.apkg. The .apkg is
+a mobile-Anki artifact the phone picks up from its sync dir, so it lands
+there rather than next to the org source.
Usage:
drill-to-anki.py <input.org>
@@ -91,15 +89,6 @@ def escape_html(s: str) -> str:
)
-def title_from_org(org_text: str) -> str | None:
- """Return the #+TITLE: value if the org file declares one."""
- for line in org_text.splitlines()[:20]:
- m = re.match(r"^#\+TITLE:\s*(.+?)\s*$", line, re.IGNORECASE)
- if m:
- return m.group(1).strip()
- return None
-
-
def strip_org_metadata(body_lines: list[str]) -> list[str]:
"""Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines.
@@ -185,13 +174,13 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck:
return deck
-def default_deck_name(input_path: Path, org_text: str) -> str:
- return title_from_org(org_text) or input_path.stem.replace("-", " ").title()
+def default_deck_name(input_path: Path) -> str:
+ return input_path.stem
def default_output_path(input_path: Path) -> Path:
- drill_dir = Path.home() / "sync" / "org" / "drill"
- return drill_dir / f"{input_path.stem}.apkg"
+ anki_dir = Path.home() / "sync" / "phone" / "anki"
+ return anki_dir / f"{input_path.stem}.apkg"
def main() -> int:
@@ -205,14 +194,13 @@ def main() -> int:
)
parser.add_argument(
"--deck",
- help="Deck name. Defaults to the org file's #+TITLE, or the "
- "input basename if no title is set.",
+ help="Deck name. Defaults to the input basename.",
)
parser.add_argument(
"--output",
type=Path,
help="Output .apkg path. Defaults to "
- "~/sync/org/drill/<input-basename>.apkg.",
+ "~/sync/phone/anki/<input-basename>.apkg.",
)
args = parser.parse_args()
@@ -222,7 +210,7 @@ def main() -> int:
return 1
org_text = input_path.read_text(encoding="utf-8")
- deck_name = args.deck or default_deck_name(input_path, org_text)
+ deck_name = args.deck or default_deck_name(input_path)
output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
diff --git a/.ai/scripts/tests/test_drill_to_anki.py b/.ai/scripts/tests/test_drill_to_anki.py
new file mode 100644
index 0000000..6490e58
--- /dev/null
+++ b/.ai/scripts/tests/test_drill_to_anki.py
@@ -0,0 +1,44 @@
+"""Tests for drill-to-anki.py default-path and deck-name helpers.
+
+The script is a PEP 723 uv-run script that imports genanki, which uv resolves
+at runtime but isn't installed in the test environment. The fixture stubs
+genanki in sys.modules so the module loads; the pure helpers under test never
+call into it.
+"""
+from __future__ import annotations
+
+import importlib.util
+import sys
+import types
+from pathlib import Path
+
+import pytest
+
+SCRIPT = Path(__file__).resolve().parents[1] / "drill-to-anki.py"
+
+
+@pytest.fixture(scope="module")
+def drill():
+ # Only stub when genanki is genuinely absent, so a real install isn't shadowed.
+ sys.modules.setdefault("genanki", types.ModuleType("genanki"))
+ spec = importlib.util.spec_from_file_location("drill_to_anki", SCRIPT)
+ assert spec and spec.loader
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def test_default_output_path_targets_phone_anki_dir(drill):
+ """The .apkg is a phone artifact, so it defaults under sync/phone/anki/."""
+ result = drill.default_output_path(Path("/home/x/projects/health/health-drill.org"))
+ assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg"
+
+
+def test_default_deck_name_is_raw_basename(drill):
+ """Deck name is the input basename with case preserved; #+TITLE is ignored."""
+ assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat"
+
+
+def test_default_deck_name_keeps_hyphens(drill):
+ """A hyphenated basename is kept verbatim rather than title-cased."""
+ assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill"
diff --git a/claude-templates/.ai/scripts/drill-to-anki.py b/claude-templates/.ai/scripts/drill-to-anki.py
index 543ccd8..1050021 100755
--- a/claude-templates/.ai/scripts/drill-to-anki.py
+++ b/claude-templates/.ai/scripts/drill-to-anki.py
@@ -13,15 +13,13 @@ Parses org-drill structure:
text (sans :drill: tag). Back = entry body with newlines converted
to <br>.
-Deck name defaults to the org file's #+TITLE if present, otherwise the
-input basename. Deck and model IDs are derived from the deck name via
-stable hash so re-importing the same deck updates existing cards
-instead of duplicating them.
+Deck name defaults to the input basename, case preserved. Deck and model
+IDs are derived from the deck name via stable hash so re-importing the
+same deck updates existing cards instead of duplicating them.
-Output defaults to ~/sync/org/drill/<input-basename>.apkg (matches
-Craig's project convention where org source lives in the project and
-symlinks into ~/sync/org/drill/, with the .apkg writing directly into
-~/sync/org/drill/ as a build artifact).
+Output defaults to ~/sync/phone/anki/<input-basename>.apkg. The .apkg is
+a mobile-Anki artifact the phone picks up from its sync dir, so it lands
+there rather than next to the org source.
Usage:
drill-to-anki.py <input.org>
@@ -91,15 +89,6 @@ def escape_html(s: str) -> str:
)
-def title_from_org(org_text: str) -> str | None:
- """Return the #+TITLE: value if the org file declares one."""
- for line in org_text.splitlines()[:20]:
- m = re.match(r"^#\+TITLE:\s*(.+?)\s*$", line, re.IGNORECASE)
- if m:
- return m.group(1).strip()
- return None
-
-
def strip_org_metadata(body_lines: list[str]) -> list[str]:
"""Drop :PROPERTIES: drawers and SCHEDULED/DEADLINE/CLOSED planning lines.
@@ -185,13 +174,13 @@ def build(cards: list[tuple[str, str, str]], deck_name: str) -> genanki.Deck:
return deck
-def default_deck_name(input_path: Path, org_text: str) -> str:
- return title_from_org(org_text) or input_path.stem.replace("-", " ").title()
+def default_deck_name(input_path: Path) -> str:
+ return input_path.stem
def default_output_path(input_path: Path) -> Path:
- drill_dir = Path.home() / "sync" / "org" / "drill"
- return drill_dir / f"{input_path.stem}.apkg"
+ anki_dir = Path.home() / "sync" / "phone" / "anki"
+ return anki_dir / f"{input_path.stem}.apkg"
def main() -> int:
@@ -205,14 +194,13 @@ def main() -> int:
)
parser.add_argument(
"--deck",
- help="Deck name. Defaults to the org file's #+TITLE, or the "
- "input basename if no title is set.",
+ help="Deck name. Defaults to the input basename.",
)
parser.add_argument(
"--output",
type=Path,
help="Output .apkg path. Defaults to "
- "~/sync/org/drill/<input-basename>.apkg.",
+ "~/sync/phone/anki/<input-basename>.apkg.",
)
args = parser.parse_args()
@@ -222,7 +210,7 @@ def main() -> int:
return 1
org_text = input_path.read_text(encoding="utf-8")
- deck_name = args.deck or default_deck_name(input_path, org_text)
+ deck_name = args.deck or default_deck_name(input_path)
output_path: Path = (args.output or default_output_path(input_path)).expanduser().resolve()
output_path.parent.mkdir(parents=True, exist_ok=True)
diff --git a/claude-templates/.ai/scripts/tests/test_drill_to_anki.py b/claude-templates/.ai/scripts/tests/test_drill_to_anki.py
new file mode 100644
index 0000000..6490e58
--- /dev/null
+++ b/claude-templates/.ai/scripts/tests/test_drill_to_anki.py
@@ -0,0 +1,44 @@
+"""Tests for drill-to-anki.py default-path and deck-name helpers.
+
+The script is a PEP 723 uv-run script that imports genanki, which uv resolves
+at runtime but isn't installed in the test environment. The fixture stubs
+genanki in sys.modules so the module loads; the pure helpers under test never
+call into it.
+"""
+from __future__ import annotations
+
+import importlib.util
+import sys
+import types
+from pathlib import Path
+
+import pytest
+
+SCRIPT = Path(__file__).resolve().parents[1] / "drill-to-anki.py"
+
+
+@pytest.fixture(scope="module")
+def drill():
+ # Only stub when genanki is genuinely absent, so a real install isn't shadowed.
+ sys.modules.setdefault("genanki", types.ModuleType("genanki"))
+ spec = importlib.util.spec_from_file_location("drill_to_anki", SCRIPT)
+ assert spec and spec.loader
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ return module
+
+
+def test_default_output_path_targets_phone_anki_dir(drill):
+ """The .apkg is a phone artifact, so it defaults under sync/phone/anki/."""
+ result = drill.default_output_path(Path("/home/x/projects/health/health-drill.org"))
+ assert result == Path.home() / "sync" / "phone" / "anki" / "health-drill.apkg"
+
+
+def test_default_deck_name_is_raw_basename(drill):
+ """Deck name is the input basename with case preserved; #+TITLE is ignored."""
+ assert drill.default_deck_name(Path("/x/deepsat.org")) == "deepsat"
+
+
+def test_default_deck_name_keeps_hyphens(drill):
+ """A hyphenated basename is kept verbatim rather than title-cased."""
+ assert drill.default_deck_name(Path("/x/health-drill.org")) == "health-drill"