Skip to content

Commit 6b3581d

Browse files
committed
merge: add update-from-file and fish completion
refer: #112
2 parents 9452a3f + 98dff27 commit 6b3581d

File tree

9 files changed

+864
-54
lines changed

9 files changed

+864
-54
lines changed

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Anki. It does not require Anki to be running at the same time.
1616
* [Usage](#usage)
1717
* [Configuration](#configuration)
1818
* [Zsh completion](#zsh-completion)
19+
* [fish completion](#fish-completion)
1920
* [Changelog](#changelog)
2021
* [Relevant resources](#relevant-resources)
2122
* [Alternatives](#alternatives)
@@ -173,6 +174,15 @@ Then add the following line to ones `.zshrc` file:
173174
fpath=($HOME/.local/zsh-functions $fpath)
174175
```
175176

177+
## Fish completion
178+
179+
There is also a fish completion file available. To use it, one may symlink or
180+
copy it to `~/.config/fish/completions/` directory:
181+
182+
```
183+
ln -s /path/to/apy/completion/apy.fish ~/.config/fish/completions/
184+
```
185+
176186
## Changelog
177187

178188
See the [release history on GitHub](https://github.com/lervag/apy/releases).

completion/_apy

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ _apy() {
4747
subcmds=( \
4848
'add:Add notes interactively from terminal' \
4949
'add-single:Add a single note from command line arguments' \
50-
'add-from-file:Add notes from Markdown file For input file' \
50+
'add-from-file:Add notes from Markdown file (alias for update-from-file)' \
51+
'update-from-file:Update existing or add new notes from Markdown file' \
5152
'check-media:Check media' \
5253
'info:Print some basic statistics' \
5354
'model:Interact with the models' \
@@ -82,10 +83,12 @@ _apy() {
8283
'::Fields' \
8384
$opts_help \
8485
);;
85-
add-from-file)
86+
add-from-file|update-from-file)
8687
opts=( \
8788
'::Markdown input file:_files -g "*.md"' \
8889
'(-t --tags)'{-t,--tags}'[Specify tags]:tags:' \
90+
'(-d --deck)'{-d,--deck}'[Specify deck]:deck:' \
91+
'(-u --update-file)'{-u,--update-file}'[Update original file with note IDs]' \
8992
$opts_help \
9093
);;
9194
info)

completion/apy.fish

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Fish shell completion for apy
2+
# Copy this file to ~/.config/fish/completions/
3+
4+
function __fish_apy_no_subcommand
5+
set -l cmd (commandline -opc)
6+
if [ (count $cmd) -eq 1 ]
7+
return 0
8+
end
9+
return 1
10+
end
11+
12+
function __fish_apy_using_command
13+
set -l cmd (commandline -opc)
14+
if [ (count $cmd) -gt 1 ]
15+
if [ $argv[1] = $cmd[2] ]
16+
return 0
17+
end
18+
end
19+
return 1
20+
end
21+
22+
# Main apy command
23+
complete -f -c apy -n '__fish_apy_no_subcommand' -l help -s h -d 'Show help'
24+
complete -f -c apy -n '__fish_apy_no_subcommand' -l base-path -s b -d 'Set Anki base directory' -a '(__fish_complete_directories)'
25+
complete -f -c apy -n '__fish_apy_no_subcommand' -l profile-name -s p -d 'Specify name of Anki profile to use'
26+
complete -f -c apy -n '__fish_apy_no_subcommand' -l version -s V -d 'Show apy version'
27+
28+
# Subcommands
29+
complete -f -c apy -n '__fish_apy_no_subcommand' -a add -d 'Add notes interactively from terminal'
30+
complete -f -c apy -n '__fish_apy_no_subcommand' -a add-single -d 'Add a single note from command line arguments'
31+
complete -f -c apy -n '__fish_apy_no_subcommand' -a add-from-file -d 'Add notes from Markdown file (alias for update-from-file)'
32+
complete -f -c apy -n '__fish_apy_no_subcommand' -a update-from-file -d 'Update existing or add new notes from Markdown file'
33+
complete -f -c apy -n '__fish_apy_no_subcommand' -a check-media -d 'Check media'
34+
complete -f -c apy -n '__fish_apy_no_subcommand' -a info -d 'Print some basic statistics'
35+
complete -f -c apy -n '__fish_apy_no_subcommand' -a model -d 'Interact with the models'
36+
complete -f -c apy -n '__fish_apy_no_subcommand' -a list -d 'Print cards that match query'
37+
complete -f -c apy -n '__fish_apy_no_subcommand' -a review -d 'Review/Edit notes that match query'
38+
complete -f -c apy -n '__fish_apy_no_subcommand' -a reposition -d 'Reposition new card with given CID'
39+
complete -f -c apy -n '__fish_apy_no_subcommand' -a sync -d 'Synchronize collection with AnkiWeb'
40+
complete -f -c apy -n '__fish_apy_no_subcommand' -a tag -d 'Add or remove tags from notes that match query'
41+
complete -f -c apy -n '__fish_apy_no_subcommand' -a edit -d 'Edit notes that match query directly'
42+
complete -f -c apy -n '__fish_apy_no_subcommand' -a backup -d 'Backup Anki database to specified target file'
43+
44+
# add options
45+
complete -f -c apy -n '__fish_apy_using_command add' -l help -s h -d 'Show help'
46+
complete -f -c apy -n '__fish_apy_using_command add' -l tags -s t -d 'Specify default tags for new cards'
47+
complete -f -c apy -n '__fish_apy_using_command add' -l model -s m -d 'Specify default model for new cards'
48+
complete -f -c apy -n '__fish_apy_using_command add' -l deck -s d -d 'Specify default deck for new cards'
49+
50+
# add-single options
51+
complete -f -c apy -n '__fish_apy_using_command add-single' -l help -s h -d 'Show help'
52+
complete -f -c apy -n '__fish_apy_using_command add-single' -l parse-markdown -s p -d 'Parse input as Markdown'
53+
complete -f -c apy -n '__fish_apy_using_command add-single' -l preset -s s -d 'Specify a preset'
54+
complete -f -c apy -n '__fish_apy_using_command add-single' -l tags -s t -d 'Specify default tags for new cards'
55+
complete -f -c apy -n '__fish_apy_using_command add-single' -l model -s m -d 'Specify default model for new cards'
56+
complete -f -c apy -n '__fish_apy_using_command add-single' -l deck -s d -d 'Specify default deck for new cards'
57+
58+
# add-from-file and update-from-file options
59+
for cmd in add-from-file update-from-file
60+
complete -f -c apy -n "__fish_apy_using_command $cmd" -l help -s h -d 'Show help'
61+
complete -f -c apy -n "__fish_apy_using_command $cmd" -l tags -s t -d 'Specify default tags for cards'
62+
complete -f -c apy -n "__fish_apy_using_command $cmd" -l deck -s d -d 'Specify default deck for cards'
63+
complete -f -c apy -n "__fish_apy_using_command $cmd" -l update-file -s u -d 'Update original file with note IDs'
64+
# File argument
65+
complete -f -c apy -n "__fish_apy_using_command $cmd" -k -a "(__fish_complete_suffix .md)"
66+
end
67+
68+
# list options
69+
complete -f -c apy -n '__fish_apy_using_command list' -l help -s h -d 'Show help'
70+
complete -f -c apy -n '__fish_apy_using_command list' -l show-answer -s a -d 'Display answer'
71+
complete -f -c apy -n '__fish_apy_using_command list' -l show-model -s m -d 'Display model'
72+
complete -f -c apy -n '__fish_apy_using_command list' -l show-cid -s c -d 'Display card ids'
73+
complete -f -c apy -n '__fish_apy_using_command list' -l show-due -s d -d 'Display card due time in days'
74+
complete -f -c apy -n '__fish_apy_using_command list' -l show-type -s t -d 'Display card type'
75+
complete -f -c apy -n '__fish_apy_using_command list' -l show-ease -s e -d 'Display card ease'
76+
complete -f -c apy -n '__fish_apy_using_command list' -l show-lapses -s l -d 'Display card number of lapses'
77+
78+
# tag options
79+
complete -f -c apy -n '__fish_apy_using_command tag' -l help -s h -d 'Show help'
80+
complete -f -c apy -n '__fish_apy_using_command tag' -l add-tags -s a -d 'Add specified tags to matched notes'
81+
complete -f -c apy -n '__fish_apy_using_command tag' -l remove-tags -s r -d 'Remove specified tags from matched notes'
82+
complete -f -c apy -n '__fish_apy_using_command tag' -l sort-by-count -s c -d 'When listing tags, sort by note count'
83+
complete -f -c apy -n '__fish_apy_using_command tag' -l purge -s p -d 'Remove all unused tags'
84+
85+
# review options
86+
complete -f -c apy -n '__fish_apy_using_command review' -l help -s h -d 'Show help'
87+
complete -f -c apy -n '__fish_apy_using_command review' -l check-markdown-consistency -s m -d 'Check for Markdown consistency'
88+
complete -f -c apy -n '__fish_apy_using_command review' -l cmc-range -s n -d 'Number of days backwards to check consistency'
89+
90+
# edit options
91+
complete -f -c apy -n '__fish_apy_using_command edit' -l help -s h -d 'Show help'
92+
complete -f -c apy -n '__fish_apy_using_command edit' -l force-multiple -s f -d 'Allow editing multiple notes'
93+
94+
# backup options
95+
complete -f -c apy -n '__fish_apy_using_command backup' -l help -s h -d 'Show help'
96+
complete -f -c apy -n '__fish_apy_using_command backup' -l include-media -s m -d 'Include media files in backup'
97+
complete -f -c apy -n '__fish_apy_using_command backup' -l legacy -s l -d 'Support older Anki versions'
98+
99+
# model subcommands
100+
complete -f -c apy -n '__fish_apy_using_command model' -a edit-css -d 'Edit the CSS template for the specified model'
101+
complete -f -c apy -n '__fish_apy_using_command model' -a rename -d 'Rename model from old_name to new_name'
102+
103+
# model edit-css options
104+
complete -f -c apy -n '__fish_apy_using_command model; and __fish_seen_subcommand_from edit-css' -l help -s h -d 'Show help'
105+
complete -f -c apy -n '__fish_apy_using_command model; and __fish_seen_subcommand_from edit-css' -l model-name -s m -d 'Specify for which model to edit CSS template'
106+
complete -f -c apy -n '__fish_apy_using_command model; and __fish_seen_subcommand_from edit-css' -l sync-after -s s -d 'Perform sync after any change'

src/apyanki/anki.py

Lines changed: 133 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import os
55
from pathlib import Path
66
import pickle
7+
import re
78
import sqlite3
89
import tempfile
910
import time
@@ -506,11 +507,139 @@ def add_notes_with_editor(
506507
return self.add_notes_from_file(tf.name)
507508

508509
def add_notes_from_file(
509-
self, filename: str, tags: str = "", deck: Optional[str] = None
510+
self,
511+
filename: str,
512+
tags: str = "",
513+
deck: Optional[str] = None,
514+
update_file: bool = False,
510515
) -> list[Note]:
511-
"""Add new notes to collection from Markdown file"""
512-
notes = markdown_file_to_notes(filename)
513-
return self.add_notes_from_list(notes, tags, deck)
516+
"""Add new notes to collection from Markdown file
517+
518+
Args:
519+
filename: Path to the markdown file containing notes
520+
tags: Additional tags to add to the notes
521+
deck: Default deck for notes without a deck specified
522+
update_file: If True, update the original file with note IDs
523+
524+
Returns:
525+
List of notes that were added
526+
"""
527+
# Reuse update_notes_from_file since it handles both adding new notes and updating existing ones
528+
# For add_notes_from_file, we're essentially just adding new notes
529+
return self.update_notes_from_file(filename, tags, deck, update_file)
530+
531+
def update_notes_from_file(
532+
self,
533+
filename: str,
534+
tags: str = "",
535+
deck: Optional[str] = None,
536+
update_file: bool = False,
537+
) -> list[Note]:
538+
"""Update existing notes or add new notes from Markdown file
539+
540+
This function looks for nid: or cid: headers in the file to determine
541+
if a note should be updated rather than added.
542+
543+
Args:
544+
filename: Path to the markdown file containing notes
545+
tags: Additional tags to add to the notes
546+
deck: Default deck for notes without a deck specified
547+
update_file: If True, update the original file with note IDs
548+
549+
Returns:
550+
List of notes that were updated or added
551+
"""
552+
with open(filename, "r", encoding="utf-8") as f:
553+
original_content = f.read()
554+
555+
notes_data = markdown_file_to_notes(filename)
556+
updated_notes = []
557+
558+
# Track if any notes were added that need IDs
559+
needs_update = False
560+
561+
for note_data in notes_data:
562+
if tags:
563+
note_data.tags = f"{tags} {note_data.tags}"
564+
565+
if deck and not note_data.deck:
566+
note_data.deck = deck
567+
568+
# Check if this note already has an ID
569+
had_id = bool(note_data.nid)
570+
571+
note = note_data.update_or_add_to_collection(self)
572+
updated_notes.append(note)
573+
574+
# Mark for file update if this was a new note without an ID
575+
if not had_id and update_file:
576+
needs_update = True
577+
578+
# Update the original file with note IDs if requested
579+
if update_file and needs_update:
580+
self._update_file_with_note_ids(filename, original_content, updated_notes)
581+
582+
return updated_notes
583+
584+
def _update_file_with_note_ids(
585+
self, filename: str, content: str, notes: list[Note]
586+
) -> None:
587+
"""Update the original markdown file with note IDs
588+
589+
This function adds nid: headers to notes in the file that don't have them.
590+
591+
Args:
592+
filename: Path to the markdown file
593+
content: Original content of the file
594+
notes: List of notes that were added/updated
595+
"""
596+
# Find all '# Note' or similar headers in the file
597+
note_headers = re.finditer(r"^# .*$", content, re.MULTILINE)
598+
note_positions = [match.start() for match in note_headers]
599+
600+
if not note_positions:
601+
return # No notes found in file
602+
603+
# Add an extra position at the end to simplify boundary handling
604+
note_positions.append(len(content))
605+
606+
# Extract each note's section and check if it needs to be updated
607+
updated_content = []
608+
for i in range(len(note_positions) - 1):
609+
start = note_positions[i]
610+
end = note_positions[i + 1]
611+
612+
# Get the section for this note
613+
section = content[start:end]
614+
615+
# Check if this section already has an nid
616+
if re.search(r"^nid:", section, re.MULTILINE):
617+
# Already has an ID, keep as is
618+
updated_content.append(section)
619+
else:
620+
# No ID, add the note ID from our updated notes
621+
# We need to find where to insert the ID line (after model, tags, etc.)
622+
lines = section.split("\n")
623+
624+
# Find a good position to insert the ID (after model, tags, deck)
625+
insert_pos = 1 # Default: after the first line (the title)
626+
for j, line in enumerate(lines[1:], 1):
627+
# Look for model:, tags:, deck: lines
628+
if re.match(r"^(model|tag[s]?|deck|markdown|md):", line):
629+
insert_pos = j + 1 # Insert after this line
630+
631+
# If we have a note ID for this position, insert it
632+
if i < len(notes):
633+
note_id = notes[i].n.id
634+
lines.insert(insert_pos, f"nid: {note_id}")
635+
updated_content.append("\n".join(lines))
636+
else:
637+
# Couldn't match this section to a note, keep unchanged
638+
updated_content.append(section)
639+
640+
# Write back the updated content
641+
with open(filename, "w", encoding="utf-8") as f:
642+
f.write("".join(updated_content))
514643

515644
def add_notes_from_list(
516645
self,

0 commit comments

Comments
 (0)