Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ currently recognized:
- `query`: Specify default query for `apy list`, `apy review` and `apy tag`.
- `review_show_cards`: Whether to show list of cards by default during note
review
- `review_verbose`: Whether to show note details by default during note
review

An example configuration:

Expand Down
35 changes: 26 additions & 9 deletions completion/_apy
Original file line number Diff line number Diff line change
Expand Up @@ -46,17 +46,19 @@ _apy() {

subcmds=( \
'add:Add notes interactively from terminal' \
'add-single:Add a single note from command line arguments' \
'add-from-file:Add notes from Markdown file (alias for update-from-file)' \
'update-from-file:Update existing or add new notes from Markdown file' \
'add-single:Add a single note from command line arguments' \
'backup:Backup Anki database to specified target file' \
'check-media:Check media' \
'edit:Edit notes that match QUERY directly' \
'info:Print some basic statistics' \
'list-cards-table:List cards that match QUERY in table format' \
'model:Interact with the models' \
'list:Print cards that match query' \
'review:Review/Edit notes that match query [default: marked/flagged]' \
'reposition:Reposition new card with given CID' \
'reposition:Reposition new cards that match QUERY' \
'review:Review/Edit notes that match QUERY [default: marked/flagged]' \
'sync:Synchronize collection with AnkiWeb' \
'tag:Add or remove tags from notes that match query' \
'tag:Add or remove tags from notes that match QUERY' \
'update-from-file:Update existing or add new notes from Markdown file' \
)

_arguments $opts '*:: :->subcmds' && return 0
Expand Down Expand Up @@ -91,10 +93,22 @@ _apy() {
'(-u --update-file)'{-u,--update-file}'[Update original file with note IDs]' \
$opts_help \
);;
backup)
opts=( \
'::Target file' \
'(-m --include-media)'{-m,--include-media}'[Include media files]:media:' \
'(-l --legacy)'{-l,--legacy}'[Support older Anki Versions]:legacy:' \
$opts_help \
);;
edit)
opts=( \
'::Query' \
'(-f --force-multiple)'{-f,--force-multiple}'[Allow editing multiple notes]' \
$opts_help \
);;
info)
opts=( $opts_help );;
model) __model; return;;
list)
list-cards-table)
opts=( \
'::Query' \
'(-w --wrap)'{-w,--wrap}'[Wrap the question and answer on multiple lines]' \
Expand All @@ -107,9 +121,12 @@ _apy() {
'(-l --show-lapses)'{-l,--show-lapses}'[Display card number of lapses]' \
$opts_help \
);;
model) __model; return;;
review)
opts=( \
'(-q --query)'{-q,--query}'[Query string]:query:' \
'::Query' \
'(-m --check-markdown-consistency)'{-m,----check-markdown-consistency}'[Check for Markdown consistency]' \
'(-n --cmc-range)'{-n,--cmc-range}'[Number of days to check]:range:' \
$opts_help \
);;
reposition)
Expand Down
20 changes: 17 additions & 3 deletions src/apyanki/anki.py
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,27 @@ def edit_model_css(self, model_name: str) -> None:
self.col.models.save(model, templates=True)
self.modified = True

def list_notes(self, query: str) -> None:
"""List notes that match a query"""
def list_note_questions(self, query: str) -> None:
"""List first card questions for notes that match a query"""
for note in self.find_notes(query):
cards.print_question(note.n.cards()[0])

def list_cards(self, query: str, opts_display: dict[str, bool]) -> None:
def list_notes(
self, query: str, show_cards: bool, show_raw_fields: bool, verbose: bool
) -> None:
"""List notes that match a query"""
for note in self.find_notes(query):
note.pprint(
print_raw=show_raw_fields, list_cards=show_cards, verbose=verbose
)

def list_cards(self, query: str, verbose: bool) -> None:
"""List cards that match a query"""
for cid in self.col.find_cards(query):
cards.card_pprint(self.col.get_card(cid), verbose)

def list_cards_as_table(self, query: str, opts_display: dict[str, bool]) -> None:
"""List cards that match a query in tabular format"""
width = console.width - 1
if opts_display.get("show_cid", False):
width -= 15
Expand Down
44 changes: 43 additions & 1 deletion src/apyanki/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,57 @@

from typing import TYPE_CHECKING

from rich.markdown import Markdown
from rich.text import Text

from apyanki.console import console
from apyanki.console import console, consolePlain
from apyanki.fields import check_if_generated_from_markdown, prepare_field_for_cli
from apyanki.fields import prepare_field_for_cli_oneline

if TYPE_CHECKING:
from anki.cards import Card


def card_pprint(card: Card, verbose: bool = True) -> None:
"""Pretty print a card."""
flag = get_flag(card)
consolePlain.print(f"[green]# Card (cid: {card.id})[/green]{flag}\n")

if verbose:
card_type = ["new", "learning", "review", "relearning"][int(card.type)]
details = [
f"[yellow]nid:[/yellow] {card.nid}",
f"[yellow]model:[/yellow] {card.note_type()['name']}",
f"[yellow]type:[/yellow] {card_type}",
f"[yellow]due:[/yellow] {card.due} days",
f"[yellow]interval:[/yellow] {card.ivl} days",
f"[yellow]repetitions:[/yellow] {card.reps}",
f"[yellow]lapses:[/yellow] {card.lapses}",
f"[yellow]ease:[/yellow] {int(card.factor / 10)} %",
"",
]
for detail in details:
consolePlain.print(detail)

rendered = card.render_output()
for title, field in [
["Front", rendered.question_text],
["Back", rendered.answer_text],
]:
is_markdown = check_if_generated_from_markdown(field)
if is_markdown:
title += " [italic](markdown)[/italic]"

console.print(f"[blue]## {title}[/blue]\n")
prepared = prepare_field_for_cli(field)
prepared = prepared.replace("\n\n", "\n")
if is_markdown:
console.print(Markdown(prepared))
else:
console.print(prepared)
console.print()


def card_field_to_text(field: str, max_width: int = 0) -> Text:
prepared_field = prepare_field_for_cli_oneline(field)
if max_width > 0:
Expand Down
60 changes: 55 additions & 5 deletions src/apyanki/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,57 @@ def rename(old_name: str, new_name: str) -> None:
a.rename_model(old_name, new_name)


@main.command("list")
@main.command("list-cards")
@click.argument("query", required=False, nargs=-1)
@click.option("-v", "--verbose", is_flag=True, help="Print details for each card")
def list_cards(query: str, verbose: bool) -> None:
"""List cards that match QUERY.

The default QUERY is "tag:marked OR -flag:0". This default can be
customized in the config file `~/.config/apy/apy.json`, e.g. with

\b
{
"query": "tag:marked OR tag:leech"
}
"""
if query:
query = " ".join(query)
else:
query = cfg["query"]

with Anki(**cfg) as a:
a.list_cards(query, verbose)


@main.command("list-notes")
@click.argument("query", required=False, nargs=-1)
@click.option("-c", "--show-cards", is_flag=True, help="Print card specs")
@click.option("-r", "--show-raw-fields", is_flag=True, help="Print raw field data")
@click.option("-v", "--verbose", is_flag=True, help="Print note details")
def list_notes(
query: str, show_cards: bool, show_raw_fields: bool, verbose: bool
) -> None:
"""List notes that match QUERY.

The default QUERY is "tag:marked OR -flag:0". This default can be
customized in the config file `~/.config/apy/apy.json`, e.g. with

\b
{
"query": "tag:marked OR tag:leech"
}
"""
if query:
query = " ".join(query)
else:
query = cfg["query"]

with Anki(**cfg) as a:
a.list_notes(query, show_cards, show_raw_fields, verbose)


@main.command("list-cards-table")
@click.argument("query", required=False, nargs=-1)
@click.option("-a", "--show-answer", is_flag=True, help="Display answer")
@click.option("-m", "--show-model", is_flag=True, help="Display model")
Expand All @@ -383,7 +433,7 @@ def rename(old_name: str, new_name: str) -> None:
@click.option("-t", "--show-type", is_flag=True, help="Display card type")
@click.option("-e", "--show-ease", is_flag=True, help="Display card ease")
@click.option("-l", "--show-lapses", is_flag=True, help="Display card number of lapses")
def list_cards(
def list_cards_table(
query: str,
show_answer: bool,
show_model: bool,
Expand All @@ -393,7 +443,7 @@ def list_cards(
show_lapses: bool,
show_cid: bool,
) -> None:
"""List cards that match QUERY.
"""List cards that match QUERY in a tabular format.

The default QUERY is "tag:marked OR -flag:0". This default can be
customized in the config file `~/.config/apy/apy.json`, e.g. with
Expand All @@ -409,7 +459,7 @@ def list_cards(
query = cfg["query"]

with Anki(**cfg) as a:
a.list_cards(
a.list_cards_as_table(
query,
{
"show_answer": show_answer,
Expand Down Expand Up @@ -656,7 +706,7 @@ def tag(
raise click.Abort()

console.print(f"The operation will be applied to {n_notes} matched notes:")
a.list_notes(query)
a.list_note_questions(query)
console.print("")

if add_tags is not None:
Expand Down
1 change: 1 addition & 0 deletions src/apyanki/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ def get_base_path() -> str | None:
"profile_name": None,
"query": "tag:marked OR -flag:0",
"review_show_cards": False,
"review_verbose": False,
}

# Ensure that cfg has required keys
Expand Down
66 changes: 40 additions & 26 deletions src/apyanki/note.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,41 +80,47 @@ def __repr__(self) -> str:

return "\n".join(lines)

def pprint(self, print_raw: bool = False, list_cards: bool = False) -> None:
def pprint(
self, print_raw: bool = False, list_cards: bool = False, verbose: bool = False
) -> None:
"""Print to screen"""
from anki import latex

header = f"[green]# Note (nid: {self.n.id})[/green]"
if self.suspended:
header += " [red](suspended)[/red]"

created = strftime("%F %H:%M", localtime(self.n.id / 1000))
modified = strftime("%F %H:%M", localtime(self.n.mod))
columned = [
f"[yellow]model:[/yellow] {self.model_name} ({len(self.n.cards())} cards)",
f"[yellow]tags:[/yellow] {self.get_tag_string()}",
f"[yellow]created:[/yellow] {created}",
f"[yellow]modified:[/yellow] {modified}",
]
if self.a.n_decks > 1:
columned += ["[yellow]deck:[/yellow] " + self.get_deck()]

if not list_cards:
flagged = [
cards.get_flag(c, str(c.template()["name"]))
for c in self.n.cards()
if c.flags > 0
consolePlain.print(header + "\n")

if verbose:
created = strftime("%F %H:%M", localtime(self.n.id / 1000))
modified = strftime("%F %H:%M", localtime(self.n.mod))
details = [
f"[yellow]model:[/yellow] {self.model_name} ({len(self.n.cards())} cards)",
f"[yellow]tags:[/yellow] {self.get_tag_string()}",
f"[yellow]created:[/yellow] {created}",
f"[yellow]modified:[/yellow] {modified}",
]
if flagged:
columned += [f"[yellow]flagged:[/yellow] {', '.join(flagged)}"]
if self.a.n_decks > 1:
details += ["[yellow]deck:[/yellow] " + self.get_deck()]

if not list_cards:
flagged = [
cards.get_flag(c, str(c.template()["name"]))
for c in self.n.cards()
if c.flags > 0
]
if flagged:
details += [f"[yellow]flagged:[/yellow] {', '.join(flagged)}"]

consolePlain.print(header)
consolePlain.print(Columns(columned, width=37))
for detail in details:
consolePlain.print(detail)

if list_cards:
self.print_cards()

console.print()
if verbose or list_cards:
console.print()

imgs: list[Path] = []
for name, field in self.n.items():
is_markdown = check_if_generated_from_markdown(field)
Expand Down Expand Up @@ -158,7 +164,7 @@ def print_cards(self) -> None:
table.add_column("Interval", justify="right", header_style="white")
table.add_column("Reps", justify="right", header_style="white")
table.add_column("Lapses", justify="right", header_style="white")
table.add_column("Factor", justify="right", header_style="white")
table.add_column("Ease", justify="right", header_style="white")
for card in sorted(self.n.cards(), key=lambda x: x.factor):
table.add_row(
"- " + str(card.template()["name"]) + cards.get_flag(card),
Expand Down Expand Up @@ -441,6 +447,7 @@ def review(
"N": "Change model",
"s": "Save and stop",
"v": "Show cards",
"V": "Show details",
"x": "Save and stop",
}

Expand Down Expand Up @@ -471,14 +478,15 @@ def review(
)

print_raw_fields = False
refresh = True
verbose = cfg["review_verbose"]
show_cards = cfg["review_show_cards"]
refresh = True
while True:
if refresh:
console.clear()
console.print(menu)
console.print("")
self.pprint(print_raw_fields, list_cards=show_cards)
self.pprint(print_raw_fields, list_cards=show_cards, verbose=verbose)

refresh = True
choice = readchar.readchar()
Expand Down Expand Up @@ -560,6 +568,12 @@ def review(

if action == "Show cards":
show_cards = not show_cards
cfg["review_show_cards"] = show_cards
continue

if action == "Show details":
verbose = not verbose
cfg["review_verbose"] = verbose
continue


Expand Down
Loading