mirror of
https://github.com/natelandau/obsidian-metadata.git
synced 2025-11-16 08:53:48 -05:00
fix(application): improve ux (#10)
* fix(vault): use table for listing notes in scope * style: alphabetize methods * fix(application): subcommand usage text formatting * fix(questions): improve question style
This commit is contained in:
@@ -29,16 +29,6 @@ class Application:
|
||||
self.dry_run = dry_run
|
||||
self.questions = Questions()
|
||||
|
||||
def load_vault(self, path_filter: str = None) -> None:
|
||||
"""Load the vault.
|
||||
|
||||
Args:
|
||||
path_filter (str, optional): Regex to filter notes by path.
|
||||
"""
|
||||
self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run, path_filter=path_filter)
|
||||
log.info(f"Indexed {self.vault.num_notes()} notes from {self.vault.vault_path}")
|
||||
self.questions = Questions(vault=self.vault)
|
||||
|
||||
def application_main(self) -> None:
|
||||
"""Questions for the main application."""
|
||||
self.load_vault()
|
||||
@@ -76,9 +66,9 @@ class Application:
|
||||
def application_add_metadata(self) -> None:
|
||||
"""Add metadata."""
|
||||
help_text = """
|
||||
[bold underline]Add Metadata[/]
|
||||
Add new metadata to your vault. Currently only supports
|
||||
adding to the frontmatter of a note.\n
|
||||
USAGE | Add Metadata
|
||||
[dim]Add new metadata to your vault. Currently only supports
|
||||
adding to the frontmatter of a note.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
@@ -114,10 +104,10 @@ class Application:
|
||||
def application_filter(self) -> None:
|
||||
"""Filter notes."""
|
||||
help_text = """
|
||||
[bold underline]Filter Notes[/]
|
||||
Enter a regex to filter notes by path. This allows you to
|
||||
specify a subset of notes to update. Leave empty to include
|
||||
all markdown files.\n
|
||||
USAGE | Filter Notes
|
||||
[dim]Enter a regex to filter notes by path. This allows you to
|
||||
specify a subset of notes to update. Leave empty to include
|
||||
all markdown files.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
@@ -151,7 +141,6 @@ class Application:
|
||||
|
||||
case "list_notes":
|
||||
self.vault.list_editable_notes()
|
||||
print("\n")
|
||||
|
||||
case _:
|
||||
return
|
||||
@@ -159,8 +148,9 @@ class Application:
|
||||
def application_inspect_metadata(self) -> None:
|
||||
"""View metadata."""
|
||||
help_text = """
|
||||
[bold underline]View Metadata[/]
|
||||
Inspect the metadata in your vault. Note, uncommitted changes will be reflected in these reports\n
|
||||
USAGE | View Metadata
|
||||
[dim]Inspect the metadata in your vault. Note, uncommitted
|
||||
changes will be reflected in these reports[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
@@ -179,8 +169,8 @@ class Application:
|
||||
def application_vault(self) -> None:
|
||||
"""Vault actions."""
|
||||
help_text = """
|
||||
[bold underline]Vault Actions[/]
|
||||
Create or delete a backup of your vault.\n
|
||||
USAGE | Vault Actions
|
||||
[dim]Create or delete a backup of your vault.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
@@ -202,8 +192,9 @@ class Application:
|
||||
|
||||
def application_delete_metadata(self) -> None:
|
||||
help_text = """
|
||||
[bold underline]Delete Metadata[/]
|
||||
Delete either a key and all associated values, or a specific value.\n
|
||||
USAGE | Delete Metadata
|
||||
[dim]Delete either a key and all associated values,
|
||||
or a specific value.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
@@ -229,8 +220,8 @@ class Application:
|
||||
def application_rename_metadata(self) -> None:
|
||||
"""Rename metadata."""
|
||||
help_text = """
|
||||
[bold underline]Rename Metadata[/]\n
|
||||
Select the type of metadata to rename.\n
|
||||
USAGE | Rename Metadata
|
||||
[dim]Select the type of metadata to rename.[/]
|
||||
"""
|
||||
print(dedent(help_text))
|
||||
|
||||
@@ -253,7 +244,33 @@ class Application:
|
||||
case _:
|
||||
return
|
||||
|
||||
###########################################################################
|
||||
def commit_changes(self) -> bool:
|
||||
"""Write all changes to disk.
|
||||
|
||||
Returns:
|
||||
True if changes were committed, False otherwise.
|
||||
"""
|
||||
changed_notes = self.vault.get_changed_notes()
|
||||
|
||||
if len(changed_notes) == 0:
|
||||
print("\n")
|
||||
alerts.notice("No changes to commit.\n")
|
||||
return False
|
||||
|
||||
backup = questionary.confirm("Create backup before committing changes").ask()
|
||||
if backup is None:
|
||||
return False
|
||||
if backup:
|
||||
self.vault.backup()
|
||||
|
||||
if questionary.confirm(f"Commit {len(changed_notes)} changed files to disk?").ask():
|
||||
|
||||
self.vault.write()
|
||||
alerts.success(f"{len(changed_notes)} changes committed to disk. Exiting")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def delete_inline_tag(self) -> None:
|
||||
"""Delete an inline tag."""
|
||||
tag = self.questions.ask_existing_inline_tag(question="Which tag would you like to delete?")
|
||||
@@ -307,6 +324,16 @@ class Application:
|
||||
|
||||
return
|
||||
|
||||
def load_vault(self, path_filter: str = None) -> None:
|
||||
"""Load the vault.
|
||||
|
||||
Args:
|
||||
path_filter (str, optional): Regex to filter notes by path.
|
||||
"""
|
||||
self.vault: Vault = Vault(config=self.config, dry_run=self.dry_run, path_filter=path_filter)
|
||||
log.info(f"Indexed {self.vault.num_notes()} notes from {self.vault.vault_path}")
|
||||
self.questions = Questions(vault=self.vault)
|
||||
|
||||
def rename_key(self) -> None:
|
||||
"""Renames a key in the vault."""
|
||||
|
||||
@@ -406,30 +433,3 @@ class Application:
|
||||
if note_to_review is None or note_to_review == "return":
|
||||
break
|
||||
changed_notes[note_to_review].print_diff()
|
||||
|
||||
def commit_changes(self) -> bool:
|
||||
"""Write all changes to disk.
|
||||
|
||||
Returns:
|
||||
True if changes were committed, False otherwise.
|
||||
"""
|
||||
changed_notes = self.vault.get_changed_notes()
|
||||
|
||||
if len(changed_notes) == 0:
|
||||
print("\n")
|
||||
alerts.notice("No changes to commit.\n")
|
||||
return False
|
||||
|
||||
backup = questionary.confirm("Create backup before committing changes").ask()
|
||||
if backup is None:
|
||||
return False
|
||||
if backup:
|
||||
self.vault.backup()
|
||||
|
||||
if questionary.confirm(f"Commit {len(changed_notes)} changed files to disk?").ask():
|
||||
|
||||
self.vault.write()
|
||||
alerts.success(f"{len(changed_notes)} changes committed to disk. Exiting")
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@@ -58,38 +58,6 @@ class VaultMetadata:
|
||||
|
||||
self.dict = dict(sorted(existing_metadata.items()))
|
||||
|
||||
def print_keys(self) -> None:
|
||||
"""Print all metadata keys."""
|
||||
columns = Columns(
|
||||
sorted(self.dict.keys()),
|
||||
equal=True,
|
||||
expand=True,
|
||||
title="All metadata keys in Obsidian vault",
|
||||
)
|
||||
print(columns)
|
||||
|
||||
def print_tags(self) -> None:
|
||||
"""Print all tags."""
|
||||
columns = Columns(
|
||||
sorted(self.dict["tags"]),
|
||||
equal=True,
|
||||
expand=True,
|
||||
title="All tags in Obsidian vault",
|
||||
)
|
||||
print(columns)
|
||||
|
||||
def print_metadata(self) -> None:
|
||||
"""Print all metadata."""
|
||||
table = Table(show_footer=False, show_lines=True)
|
||||
table.add_column("Keys")
|
||||
table.add_column("Values")
|
||||
for key, value in sorted(self.dict.items()):
|
||||
values: str | dict[str, list[str]] = (
|
||||
"\n".join(sorted(value)) if isinstance(value, list) else value
|
||||
)
|
||||
table.add_row(f"[bold]{key}[/]", str(values))
|
||||
Console().print(table)
|
||||
|
||||
def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if a key and/or a value exists in the metadata.
|
||||
|
||||
@@ -131,6 +99,38 @@ class VaultMetadata:
|
||||
|
||||
return False
|
||||
|
||||
def print_keys(self) -> None:
|
||||
"""Print all metadata keys."""
|
||||
columns = Columns(
|
||||
sorted(self.dict.keys()),
|
||||
equal=True,
|
||||
expand=True,
|
||||
title="All metadata keys in Obsidian vault",
|
||||
)
|
||||
print(columns)
|
||||
|
||||
def print_metadata(self) -> None:
|
||||
"""Print all metadata."""
|
||||
table = Table(show_footer=False, show_lines=True)
|
||||
table.add_column("Keys")
|
||||
table.add_column("Values")
|
||||
for key, value in sorted(self.dict.items()):
|
||||
values: str | dict[str, list[str]] = (
|
||||
"\n".join(sorted(value)) if isinstance(value, list) else value
|
||||
)
|
||||
table.add_row(f"[bold]{key}[/]", str(values))
|
||||
Console().print(table)
|
||||
|
||||
def print_tags(self) -> None:
|
||||
"""Print all tags."""
|
||||
columns = Columns(
|
||||
sorted(self.dict["tags"]),
|
||||
equal=True,
|
||||
expand=True,
|
||||
title="All tags in Obsidian vault",
|
||||
)
|
||||
print(columns)
|
||||
|
||||
def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Replace a value in the frontmatter.
|
||||
|
||||
@@ -244,29 +244,6 @@ class Frontmatter:
|
||||
"""
|
||||
return dict_contains(self.dict, key, value, is_regex)
|
||||
|
||||
def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Replace a value in the frontmatter.
|
||||
|
||||
Args:
|
||||
key (str): Key to check.
|
||||
value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
value_2 (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def delete(self, key: str, value_to_delete: str = None) -> bool:
|
||||
"""Delete a value or key in the frontmatter. Regex is supported to allow deleting more than one key or value.
|
||||
|
||||
@@ -303,6 +280,29 @@ class Frontmatter:
|
||||
"""
|
||||
return self.dict != self.dict_original
|
||||
|
||||
def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Replace a value in the frontmatter.
|
||||
|
||||
Args:
|
||||
key (str): Key to check.
|
||||
value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
value_2 (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def to_yaml(self, sort_keys: bool = False) -> str:
|
||||
"""Return the frontmatter as a YAML string.
|
||||
|
||||
@@ -348,19 +348,6 @@ class InlineMetadata:
|
||||
"""
|
||||
return f"InlineMetadata(inline_metadata={self.dict})"
|
||||
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool:
|
||||
"""Add a key and value to the frontmatter.
|
||||
|
||||
Args:
|
||||
key (str): Key to add.
|
||||
value (str, optional): Value to add.
|
||||
|
||||
Returns:
|
||||
bool: True if the metadata was added
|
||||
"""
|
||||
# TODO: implement adding to inline metadata which requires knowing where in the note the metadata is to be added. In addition, unlike frontmatter, it is not possible to have multiple values for a key.
|
||||
pass
|
||||
|
||||
def _grab_inline_metadata(self, file_content: str) -> dict[str, list[str]]:
|
||||
"""Grab inline metadata from a note.
|
||||
|
||||
@@ -385,6 +372,19 @@ class InlineMetadata:
|
||||
|
||||
return clean_dictionary(inline_metadata)
|
||||
|
||||
def add(self, key: str, value: str | list[str] = None) -> bool:
|
||||
"""Add a key and value to the frontmatter.
|
||||
|
||||
Args:
|
||||
key (str): Key to add.
|
||||
value (str, optional): Value to add.
|
||||
|
||||
Returns:
|
||||
bool: True if the metadata was added
|
||||
"""
|
||||
# TODO: implement adding to inline metadata which requires knowing where in the note the metadata is to be added. In addition, unlike frontmatter, it is not possible to have multiple values for a key.
|
||||
pass
|
||||
|
||||
def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool:
|
||||
"""Check if a key or value exists in the inline metadata.
|
||||
|
||||
@@ -398,29 +398,6 @@ class InlineMetadata:
|
||||
"""
|
||||
return dict_contains(self.dict, key, value, is_regex)
|
||||
|
||||
def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Replace a value in the inline metadata.
|
||||
|
||||
Args:
|
||||
key (str): Key to check.
|
||||
value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
value_2 (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def delete(self, key: str, value_to_delete: str = None) -> bool:
|
||||
"""Delete a value or key in the inline metadata. Regex is supported to allow deleting more than one key or value.
|
||||
|
||||
@@ -457,6 +434,29 @@ class InlineMetadata:
|
||||
"""
|
||||
return self.dict != self.dict_original
|
||||
|
||||
def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
|
||||
"""Replace a value in the inline metadata.
|
||||
|
||||
Args:
|
||||
key (str): Key to check.
|
||||
value_1 (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
value_2 (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if value_2 is None:
|
||||
if key in self.dict and value_1 not in self.dict:
|
||||
self.dict[value_1] = self.dict.pop(key)
|
||||
return True
|
||||
return False
|
||||
|
||||
if key in self.dict and value_1 in self.dict[key]:
|
||||
self.dict[key] = sorted({value_2 if x == value_1 else x for x in self.dict[key]})
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
class InlineTags:
|
||||
"""Representation of inline tags."""
|
||||
@@ -512,21 +512,6 @@ class InlineTags:
|
||||
|
||||
return False
|
||||
|
||||
def rename(self, old_tag: str, new_tag: str) -> bool:
|
||||
"""Replace an inline tag with another string.
|
||||
|
||||
Args:
|
||||
old_tag (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
new_tag (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if old_tag in self.list:
|
||||
self.list = sorted([new_tag if i == old_tag else i for i in self.list])
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete(self, tag_to_delete: str) -> bool:
|
||||
"""Delete a specified inline tag. Regex is supported to allow deleting more than one tag.
|
||||
|
||||
@@ -550,3 +535,18 @@ class InlineTags:
|
||||
bool: True if the metadata has changes.
|
||||
"""
|
||||
return self.list != self.list_original
|
||||
|
||||
def rename(self, old_tag: str, new_tag: str) -> bool:
|
||||
"""Replace an inline tag with another string.
|
||||
|
||||
Args:
|
||||
old_tag (str): `With value_2` this is the value to rename. If `value_2` is None this is the renamed key
|
||||
new_tag (str, Optional): New value.
|
||||
|
||||
Returns:
|
||||
bool: True if a value was renamed
|
||||
"""
|
||||
if old_tag in self.list:
|
||||
self.list = sorted([new_tag if i == old_tag else i for i in self.list])
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -64,6 +64,59 @@ class Note:
|
||||
yield "inline_tags", self.inline_tags
|
||||
yield "inline_metadata", self.inline_metadata
|
||||
|
||||
def _delete_inline_metadata(self, key: str, value: str = None) -> None:
|
||||
"""Deletes an inline metadata key/value pair from the text of the note. This method does not remove the key/value from the metadata attribute of the note.
|
||||
|
||||
Args:
|
||||
key (str): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for (_k, _v) in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value is None:
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(rf"\[?{_k}:: ?{_v}]?", "", is_regex=True)
|
||||
return
|
||||
|
||||
if re.search(value, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True)
|
||||
|
||||
def _rename_inline_metadata(self, key: str, value_1: str, value_2: str = None) -> None:
|
||||
"""Replaces the inline metadata in the note with the current inline metadata object.
|
||||
|
||||
Args:
|
||||
key (str): Key to rename.
|
||||
value_1 (str): Value to replace OR new key name (if value_2 is None).
|
||||
value_2 (str, optional): New value.
|
||||
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for (_k, _v) in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value_2 is None:
|
||||
if re.search(rf"{key}[^\w\d_-]+", _k):
|
||||
key_text = re.split(r"[^\w\d_-]+$", _k)[0]
|
||||
key_markdown = re.split(r"^[\w\d_-]+", _k)[1]
|
||||
self.sub(
|
||||
rf"{key_text}{key_markdown}::",
|
||||
rf"{value_1}{key_markdown}::",
|
||||
)
|
||||
else:
|
||||
self.sub(f"{_k}::", f"{value_1}::")
|
||||
else:
|
||||
if re.search(key, _k) and re.search(value_1, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True)
|
||||
|
||||
def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> bool:
|
||||
"""Adds metadata to the note.
|
||||
|
||||
@@ -144,29 +197,6 @@ class Note:
|
||||
|
||||
return False
|
||||
|
||||
def _delete_inline_metadata(self, key: str, value: str = None) -> None:
|
||||
"""Deletes an inline metadata key/value pair from the text of the note. This method does not remove the key/value from the metadata attribute of the note.
|
||||
|
||||
Args:
|
||||
key (str): Key to delete.
|
||||
value (str, optional): Value to delete.
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for (_k, _v) in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value is None:
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(rf"\[?{_k}:: ?{_v}]?", "", is_regex=True)
|
||||
return
|
||||
|
||||
if re.search(value, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(rf"({_k}::) ?{_v}", r"\1", is_regex=True)
|
||||
|
||||
def delete_inline_tag(self, tag: str) -> bool:
|
||||
"""Deletes an inline tag from the `inline_tags` attribute AND removes the tag from the text of the note if it exists.
|
||||
|
||||
@@ -263,48 +293,27 @@ class Note:
|
||||
|
||||
Console().print(table)
|
||||
|
||||
def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None:
|
||||
"""Substitutes text within the note.
|
||||
def replace_frontmatter(self, sort_keys: bool = False) -> None:
|
||||
"""Replaces the frontmatter in the note with the current frontmatter object."""
|
||||
try:
|
||||
current_frontmatter = PATTERNS.frontmatt_block_with_separators.search(
|
||||
self.file_content
|
||||
).group("frontmatter")
|
||||
except AttributeError:
|
||||
current_frontmatter = None
|
||||
|
||||
Args:
|
||||
pattern (str): The pattern to replace (plain text or regular expression).
|
||||
replacement (str): What to replace the pattern with.
|
||||
is_regex (bool): Whether the pattern is a regex pattern or plain text.
|
||||
"""
|
||||
if not is_regex:
|
||||
pattern = re.escape(pattern)
|
||||
if current_frontmatter is None and self.frontmatter.dict == {}:
|
||||
return
|
||||
|
||||
self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE)
|
||||
new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys)
|
||||
new_frontmatter = f"---\n{new_frontmatter}---\n"
|
||||
|
||||
def _rename_inline_metadata(self, key: str, value_1: str, value_2: str = None) -> None:
|
||||
"""Replaces the inline metadata in the note with the current inline metadata object.
|
||||
if current_frontmatter is None:
|
||||
self.file_content = new_frontmatter + self.file_content
|
||||
return
|
||||
|
||||
Args:
|
||||
key (str): Key to rename.
|
||||
value_1 (str): Value to replace OR new key name (if value_2 is None).
|
||||
value_2 (str, optional): New value.
|
||||
|
||||
"""
|
||||
all_results = PATTERNS.find_inline_metadata.findall(self.file_content)
|
||||
stripped_null_values = [tuple(filter(None, x)) for x in all_results]
|
||||
|
||||
for (_k, _v) in stripped_null_values:
|
||||
if re.search(key, _k):
|
||||
if value_2 is None:
|
||||
if re.search(rf"{key}[^\w\d_-]+", _k):
|
||||
key_text = re.split(r"[^\w\d_-]+$", _k)[0]
|
||||
key_markdown = re.split(r"^[\w\d_-]+", _k)[1]
|
||||
self.sub(
|
||||
rf"{key_text}{key_markdown}::",
|
||||
rf"{value_1}{key_markdown}::",
|
||||
)
|
||||
else:
|
||||
self.sub(f"{_k}::", f"{value_1}::")
|
||||
else:
|
||||
if re.search(key, _k) and re.search(value_1, _v):
|
||||
_k = re.escape(_k)
|
||||
_v = re.escape(_v)
|
||||
self.sub(f"{_k}:: ?{_v}", f"{_k}:: {value_2}", is_regex=True)
|
||||
current_frontmatter = re.escape(current_frontmatter)
|
||||
self.sub(current_frontmatter, new_frontmatter, is_regex=True)
|
||||
|
||||
def rename_inline_tag(self, tag_1: str, tag_2: str) -> bool:
|
||||
"""Renames an inline tag from the note ONLY if it's not in the metadata as well.
|
||||
@@ -360,27 +369,18 @@ class Note:
|
||||
|
||||
return False
|
||||
|
||||
def replace_frontmatter(self, sort_keys: bool = False) -> None:
|
||||
"""Replaces the frontmatter in the note with the current frontmatter object."""
|
||||
try:
|
||||
current_frontmatter = PATTERNS.frontmatt_block_with_separators.search(
|
||||
self.file_content
|
||||
).group("frontmatter")
|
||||
except AttributeError:
|
||||
current_frontmatter = None
|
||||
def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None:
|
||||
"""Substitutes text within the note.
|
||||
|
||||
if current_frontmatter is None and self.frontmatter.dict == {}:
|
||||
return
|
||||
Args:
|
||||
pattern (str): The pattern to replace (plain text or regular expression).
|
||||
replacement (str): What to replace the pattern with.
|
||||
is_regex (bool): Whether the pattern is a regex pattern or plain text.
|
||||
"""
|
||||
if not is_regex:
|
||||
pattern = re.escape(pattern)
|
||||
|
||||
new_frontmatter = self.frontmatter.to_yaml(sort_keys=sort_keys)
|
||||
new_frontmatter = f"---\n{new_frontmatter}---\n"
|
||||
|
||||
if current_frontmatter is None:
|
||||
self.file_content = new_frontmatter + self.file_content
|
||||
return
|
||||
|
||||
current_frontmatter = re.escape(current_frontmatter)
|
||||
self.sub(current_frontmatter, new_frontmatter, is_regex=True)
|
||||
self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE)
|
||||
|
||||
def write(self, path: Path = None) -> None:
|
||||
"""Writes the note's content to disk.
|
||||
|
||||
@@ -17,13 +17,6 @@ class Patterns:
|
||||
re.MULTILINE | re.X,
|
||||
)
|
||||
|
||||
frontmatt_block_with_separators: Pattern[str] = re.compile(
|
||||
r"^\s*(?P<frontmatter>---.*?---)", flags=re.DOTALL
|
||||
)
|
||||
frontmatt_block_no_separators: Pattern[str] = re.compile(
|
||||
r"^\s*---(?P<frontmatter>.*?)---", flags=re.DOTALL
|
||||
)
|
||||
# This pattern will return a tuple of 4 values, two will be empty and will need to be stripped before processing further
|
||||
find_inline_metadata: Pattern[str] = re.compile(
|
||||
r""" # First look for in-text key values
|
||||
(?:^\[| \[) # Find key with starting bracket
|
||||
@@ -37,5 +30,13 @@ class Patterns:
|
||||
re.X | re.MULTILINE,
|
||||
)
|
||||
|
||||
validate_tag_text: Pattern[str] = re.compile(r"[ \|,;:\*\(\)\[\]\\\.\n#&]")
|
||||
frontmatt_block_with_separators: Pattern[str] = re.compile(
|
||||
r"^\s*(?P<frontmatter>---.*?---)", flags=re.DOTALL
|
||||
)
|
||||
frontmatt_block_no_separators: Pattern[str] = re.compile(
|
||||
r"^\s*---(?P<frontmatter>.*?)---", flags=re.DOTALL
|
||||
)
|
||||
# This pattern will return a tuple of 4 values, two will be empty and will need to be stripped before processing further
|
||||
|
||||
validate_key_text: Pattern[str] = re.compile(r"[^-_\w\d\/\*\u263a-\U0001f645]")
|
||||
validate_tag_text: Pattern[str] = re.compile(r"[ \|,;:\*\(\)\[\]\\\.\n#&]")
|
||||
|
||||
@@ -64,9 +64,12 @@ class Questions:
|
||||
"""
|
||||
self.style = questionary.Style(
|
||||
[
|
||||
("separator", "bold fg:#6C6C6C"),
|
||||
("instruction", "fg:#6C6C6C"),
|
||||
("highlighted", "bold reverse"),
|
||||
("qmark", "fg:#808080 bold"),
|
||||
("question", "bold"),
|
||||
("separator", "fg:#808080"),
|
||||
("instruction", "fg:#808080"),
|
||||
("highlighted", "fg:#c0c0c0 bold reverse"),
|
||||
("text", ""),
|
||||
("pointer", "bold"),
|
||||
]
|
||||
)
|
||||
@@ -241,7 +244,10 @@ class Questions:
|
||||
|
||||
choices.append(questionary.Separator()) # type: ignore [arg-type]
|
||||
choices.append({"name": "Cancel", "value": "cancel"})
|
||||
return self.ask_selection(choices=choices, question="Select the type of metadata")
|
||||
return self.ask_selection(
|
||||
choices=choices,
|
||||
question="Select the type of metadata",
|
||||
)
|
||||
|
||||
def ask_confirm(self, question: str, default: bool = True) -> bool: # pragma: no cover
|
||||
"""Ask the user to confirm an action.
|
||||
@@ -253,13 +259,17 @@ class Questions:
|
||||
Returns:
|
||||
bool: True if the user confirms, otherwise False.
|
||||
"""
|
||||
return questionary.confirm(question, default=default, style=self.style).ask()
|
||||
return questionary.confirm(
|
||||
question, default=default, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_existing_inline_tag(self, question: str = "Enter a tag") -> str: # pragma: no cover
|
||||
"""Ask the user for an existing inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_existing_inline_tag,
|
||||
style=self.style,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
|
||||
def ask_existing_key(self, question: str = "Enter a key") -> str: # pragma: no cover
|
||||
@@ -272,8 +282,7 @@ class Questions:
|
||||
str: A metadata key that exists in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_key_exists,
|
||||
question, validate=self._validate_key_exists, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_existing_keys_regex(self, question: str = "Regex for keys") -> str: # pragma: no cover
|
||||
@@ -286,8 +295,7 @@ class Questions:
|
||||
str: A regex for metadata keys that exist in the vault.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_key_exists_regex,
|
||||
question, validate=self._validate_key_exists_regex, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_existing_value(self, question: str = "Enter a value") -> str: # pragma: no cover
|
||||
@@ -299,7 +307,9 @@ class Questions:
|
||||
Returns:
|
||||
str: A metadata value.
|
||||
"""
|
||||
return questionary.text(question, validate=self._validate_value).ask()
|
||||
return questionary.text(
|
||||
question, validate=self._validate_value, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_filter_path(self) -> str: # pragma: no cover
|
||||
"""Ask the user for the path to the filter file.
|
||||
@@ -311,6 +321,8 @@ class Questions:
|
||||
"Regex to filter the notes being processed by their path:",
|
||||
only_directories=False,
|
||||
validate=self._validate_valid_vault_regex,
|
||||
style=self.style,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
if filter_path_regex is None:
|
||||
raise typer.Exit(code=1)
|
||||
@@ -331,6 +343,8 @@ class Questions:
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_value_exists_regex,
|
||||
style=self.style,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
|
||||
def ask_application_main(self) -> str: # pragma: no cover
|
||||
@@ -345,7 +359,7 @@ class Questions:
|
||||
return questionary.select(
|
||||
"What do you want to do?",
|
||||
choices=[
|
||||
{"name": "Vault Actions, ", "value": "vault_actions"},
|
||||
{"name": "Vault Actions", "value": "vault_actions"},
|
||||
{"name": "Inspect Metadata", "value": "inspect_metadata"},
|
||||
{"name": "Filter Notes in Scope", "value": "filter_notes"},
|
||||
{"name": "Add Metadata", "value": "add_metadata"},
|
||||
@@ -359,6 +373,7 @@ class Questions:
|
||||
],
|
||||
use_shortcuts=False,
|
||||
style=self.style,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
|
||||
def ask_new_key(self, question: str = "New key name") -> str: # pragma: no cover
|
||||
@@ -371,15 +386,13 @@ class Questions:
|
||||
str: A new metadata key.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_key,
|
||||
question, validate=self._validate_new_key, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_new_tag(self, question: str = "New tag name") -> str: # pragma: no cover
|
||||
"""Ask the user for a new inline tag."""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_tag,
|
||||
question, validate=self._validate_new_tag, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_new_value(self, question: str = "New value") -> str: # pragma: no cover
|
||||
@@ -392,8 +405,7 @@ class Questions:
|
||||
str: A new metadata value.
|
||||
"""
|
||||
return questionary.text(
|
||||
question,
|
||||
validate=self._validate_new_value,
|
||||
question, validate=self._validate_new_value, style=self.style, qmark="INPUT |"
|
||||
).ask()
|
||||
|
||||
def ask_selection(
|
||||
@@ -413,4 +425,5 @@ class Questions:
|
||||
choices=choices,
|
||||
use_shortcuts=False,
|
||||
style=self.style,
|
||||
qmark="INPUT |",
|
||||
).ask()
|
||||
|
||||
@@ -5,6 +5,7 @@ import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import rich.repr
|
||||
from rich import box
|
||||
from rich.console import Console
|
||||
from rich.progress import Progress, SpinnerColumn, TextColumn
|
||||
from rich.prompt import Confirm
|
||||
@@ -124,6 +125,7 @@ class Vault:
|
||||
log.debug("Backing up vault")
|
||||
if self.dry_run:
|
||||
alerts.dryrun(f"Backup up vault to: {self.backup_path}")
|
||||
print("\n")
|
||||
return
|
||||
|
||||
try:
|
||||
@@ -252,8 +254,10 @@ class Vault:
|
||||
|
||||
def list_editable_notes(self) -> None:
|
||||
"""Print a list of notes within the scope that are being edited."""
|
||||
for _note in self.notes:
|
||||
print(_note.note_path.relative_to(self.vault_path))
|
||||
table = Table(title="Notes in current scope", show_header=False, box=box.HORIZONTALS)
|
||||
for _n, _note in enumerate(self.notes, start=1):
|
||||
table.add_row(str(_n), str(_note.note_path.relative_to(self.vault_path)))
|
||||
Console().print(table)
|
||||
|
||||
def num_excluded_notes(self) -> int:
|
||||
"""Count number of excluded notes."""
|
||||
|
||||
@@ -160,6 +160,19 @@ def test_info(test_vault, capsys):
|
||||
assert captured.out == Regex(r"Backup +\│ None")
|
||||
|
||||
|
||||
def test_list_editable_notes(test_vault, capsys) -> None:
|
||||
"""Test listing editable notes."""
|
||||
vault_path = test_vault
|
||||
config = Config(config_path="tests/fixtures/test_vault_config.toml", vault_path=vault_path)
|
||||
vault_config = config.vaults[0]
|
||||
vault = Vault(config=vault_config)
|
||||
|
||||
vault.list_editable_notes()
|
||||
captured = capsys.readouterr()
|
||||
assert captured.out == Regex("Notes in current scope")
|
||||
assert captured.out == Regex(r"1 +test1\.md")
|
||||
|
||||
|
||||
def test_contains_inline_tag(test_vault) -> None:
|
||||
"""Test if the vault contains an inline tag."""
|
||||
vault_path = test_vault
|
||||
|
||||
Reference in New Issue
Block a user