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:
Nathaniel Landau
2023-01-30 13:29:18 -05:00
committed by GitHub
parent 48174ebde9
commit c0d37eff3b
7 changed files with 298 additions and 267 deletions

View File

@@ -29,16 +29,6 @@ class Application:
self.dry_run = dry_run self.dry_run = dry_run
self.questions = Questions() 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: def application_main(self) -> None:
"""Questions for the main application.""" """Questions for the main application."""
self.load_vault() self.load_vault()
@@ -76,9 +66,9 @@ class Application:
def application_add_metadata(self) -> None: def application_add_metadata(self) -> None:
"""Add metadata.""" """Add metadata."""
help_text = """ help_text = """
[bold underline]Add Metadata[/] USAGE | Add Metadata
Add new metadata to your vault. Currently only supports [dim]Add new metadata to your vault. Currently only supports
adding to the frontmatter of a note.\n adding to the frontmatter of a note.[/]
""" """
print(dedent(help_text)) print(dedent(help_text))
@@ -114,10 +104,10 @@ class Application:
def application_filter(self) -> None: def application_filter(self) -> None:
"""Filter notes.""" """Filter notes."""
help_text = """ help_text = """
[bold underline]Filter Notes[/] USAGE | Filter Notes
Enter a regex to filter notes by path. This allows you to [dim]Enter a regex to filter notes by path. This allows you to
specify a subset of notes to update. Leave empty to include specify a subset of notes to update. Leave empty to include
all markdown files.\n all markdown files.[/]
""" """
print(dedent(help_text)) print(dedent(help_text))
@@ -151,7 +141,6 @@ class Application:
case "list_notes": case "list_notes":
self.vault.list_editable_notes() self.vault.list_editable_notes()
print("\n")
case _: case _:
return return
@@ -159,8 +148,9 @@ class Application:
def application_inspect_metadata(self) -> None: def application_inspect_metadata(self) -> None:
"""View metadata.""" """View metadata."""
help_text = """ help_text = """
[bold underline]View Metadata[/] USAGE | View Metadata
Inspect the metadata in your vault. Note, uncommitted changes will be reflected in these reports\n [dim]Inspect the metadata in your vault. Note, uncommitted
changes will be reflected in these reports[/]
""" """
print(dedent(help_text)) print(dedent(help_text))
@@ -179,8 +169,8 @@ class Application:
def application_vault(self) -> None: def application_vault(self) -> None:
"""Vault actions.""" """Vault actions."""
help_text = """ help_text = """
[bold underline]Vault Actions[/] USAGE | Vault Actions
Create or delete a backup of your vault.\n [dim]Create or delete a backup of your vault.[/]
""" """
print(dedent(help_text)) print(dedent(help_text))
@@ -202,8 +192,9 @@ class Application:
def application_delete_metadata(self) -> None: def application_delete_metadata(self) -> None:
help_text = """ help_text = """
[bold underline]Delete Metadata[/] USAGE | Delete Metadata
Delete either a key and all associated values, or a specific value.\n [dim]Delete either a key and all associated values,
or a specific value.[/]
""" """
print(dedent(help_text)) print(dedent(help_text))
@@ -229,8 +220,8 @@ class Application:
def application_rename_metadata(self) -> None: def application_rename_metadata(self) -> None:
"""Rename metadata.""" """Rename metadata."""
help_text = """ help_text = """
[bold underline]Rename Metadata[/]\n USAGE | Rename Metadata
Select the type of metadata to rename.\n [dim]Select the type of metadata to rename.[/]
""" """
print(dedent(help_text)) print(dedent(help_text))
@@ -253,7 +244,33 @@ class Application:
case _: case _:
return 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: def delete_inline_tag(self) -> None:
"""Delete an inline tag.""" """Delete an inline tag."""
tag = self.questions.ask_existing_inline_tag(question="Which tag would you like to delete?") tag = self.questions.ask_existing_inline_tag(question="Which tag would you like to delete?")
@@ -307,6 +324,16 @@ class Application:
return 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: def rename_key(self) -> None:
"""Renames a key in the vault.""" """Renames a key in the vault."""
@@ -406,30 +433,3 @@ class Application:
if note_to_review is None or note_to_review == "return": if note_to_review is None or note_to_review == "return":
break break
changed_notes[note_to_review].print_diff() 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

View File

@@ -58,38 +58,6 @@ class VaultMetadata:
self.dict = dict(sorted(existing_metadata.items())) 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: 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. """Check if a key and/or a value exists in the metadata.
@@ -131,6 +99,38 @@ class VaultMetadata:
return False 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: def rename(self, key: str, value_1: str, value_2: str = None) -> bool:
"""Replace a value in the frontmatter. """Replace a value in the frontmatter.
@@ -244,29 +244,6 @@ class Frontmatter:
""" """
return dict_contains(self.dict, key, value, is_regex) 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: 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. """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 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: def to_yaml(self, sort_keys: bool = False) -> str:
"""Return the frontmatter as a YAML string. """Return the frontmatter as a YAML string.
@@ -348,19 +348,6 @@ class InlineMetadata:
""" """
return f"InlineMetadata(inline_metadata={self.dict})" 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]]: def _grab_inline_metadata(self, file_content: str) -> dict[str, list[str]]:
"""Grab inline metadata from a note. """Grab inline metadata from a note.
@@ -385,6 +372,19 @@ class InlineMetadata:
return clean_dictionary(inline_metadata) 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: def contains(self, key: str, value: str = None, is_regex: bool = False) -> bool:
"""Check if a key or value exists in the inline metadata. """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) 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: 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. """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 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: class InlineTags:
"""Representation of inline tags.""" """Representation of inline tags."""
@@ -512,21 +512,6 @@ class InlineTags:
return False 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: def delete(self, tag_to_delete: str) -> bool:
"""Delete a specified inline tag. Regex is supported to allow deleting more than one tag. """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. bool: True if the metadata has changes.
""" """
return self.list != self.list_original 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

View File

@@ -64,6 +64,59 @@ class Note:
yield "inline_tags", self.inline_tags yield "inline_tags", self.inline_tags
yield "inline_metadata", self.inline_metadata 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: def add_metadata(self, area: MetadataType, key: str, value: str | list[str] = None) -> bool:
"""Adds metadata to the note. """Adds metadata to the note.
@@ -144,29 +197,6 @@ class Note:
return False 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: 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. """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) Console().print(table)
def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None: def replace_frontmatter(self, sort_keys: bool = False) -> None:
"""Substitutes text within the note. """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: if current_frontmatter is None and self.frontmatter.dict == {}:
pattern (str): The pattern to replace (plain text or regular expression). return
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)
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: if current_frontmatter is None:
"""Replaces the inline metadata in the note with the current inline metadata object. self.file_content = new_frontmatter + self.file_content
return
Args: current_frontmatter = re.escape(current_frontmatter)
key (str): Key to rename. self.sub(current_frontmatter, new_frontmatter, is_regex=True)
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 rename_inline_tag(self, tag_1: str, tag_2: str) -> bool: 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. """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 return False
def replace_frontmatter(self, sort_keys: bool = False) -> None: def sub(self, pattern: str, replacement: str, is_regex: bool = False) -> None:
"""Replaces the frontmatter in the note with the current frontmatter object.""" """Substitutes text within the note.
try:
current_frontmatter = PATTERNS.frontmatt_block_with_separators.search(
self.file_content
).group("frontmatter")
except AttributeError:
current_frontmatter = None
if current_frontmatter is None and self.frontmatter.dict == {}: Args:
return 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) self.file_content = re.sub(pattern, replacement, self.file_content, re.MULTILINE)
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)
def write(self, path: Path = None) -> None: def write(self, path: Path = None) -> None:
"""Writes the note's content to disk. """Writes the note's content to disk.

View File

@@ -17,13 +17,6 @@ class Patterns:
re.MULTILINE | re.X, 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( find_inline_metadata: Pattern[str] = re.compile(
r""" # First look for in-text key values r""" # First look for in-text key values
(?:^\[| \[) # Find key with starting bracket (?:^\[| \[) # Find key with starting bracket
@@ -37,5 +30,13 @@ class Patterns:
re.X | re.MULTILINE, 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_key_text: Pattern[str] = re.compile(r"[^-_\w\d\/\*\u263a-\U0001f645]")
validate_tag_text: Pattern[str] = re.compile(r"[ \|,;:\*\(\)\[\]\\\.\n#&]")

View File

@@ -64,9 +64,12 @@ class Questions:
""" """
self.style = questionary.Style( self.style = questionary.Style(
[ [
("separator", "bold fg:#6C6C6C"), ("qmark", "fg:#808080 bold"),
("instruction", "fg:#6C6C6C"), ("question", "bold"),
("highlighted", "bold reverse"), ("separator", "fg:#808080"),
("instruction", "fg:#808080"),
("highlighted", "fg:#c0c0c0 bold reverse"),
("text", ""),
("pointer", "bold"), ("pointer", "bold"),
] ]
) )
@@ -241,7 +244,10 @@ class Questions:
choices.append(questionary.Separator()) # type: ignore [arg-type] choices.append(questionary.Separator()) # type: ignore [arg-type]
choices.append({"name": "Cancel", "value": "cancel"}) 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 def ask_confirm(self, question: str, default: bool = True) -> bool: # pragma: no cover
"""Ask the user to confirm an action. """Ask the user to confirm an action.
@@ -253,13 +259,17 @@ class Questions:
Returns: Returns:
bool: True if the user confirms, otherwise False. 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 def ask_existing_inline_tag(self, question: str = "Enter a tag") -> str: # pragma: no cover
"""Ask the user for an existing inline tag.""" """Ask the user for an existing inline tag."""
return questionary.text( return questionary.text(
question, question,
validate=self._validate_existing_inline_tag, validate=self._validate_existing_inline_tag,
style=self.style,
qmark="INPUT |",
).ask() ).ask()
def ask_existing_key(self, question: str = "Enter a key") -> str: # pragma: no cover 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. str: A metadata key that exists in the vault.
""" """
return questionary.text( return questionary.text(
question, question, validate=self._validate_key_exists, style=self.style, qmark="INPUT |"
validate=self._validate_key_exists,
).ask() ).ask()
def ask_existing_keys_regex(self, question: str = "Regex for keys") -> str: # pragma: no cover 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. str: A regex for metadata keys that exist in the vault.
""" """
return questionary.text( return questionary.text(
question, question, validate=self._validate_key_exists_regex, style=self.style, qmark="INPUT |"
validate=self._validate_key_exists_regex,
).ask() ).ask()
def ask_existing_value(self, question: str = "Enter a value") -> str: # pragma: no cover def ask_existing_value(self, question: str = "Enter a value") -> str: # pragma: no cover
@@ -299,7 +307,9 @@ class Questions:
Returns: Returns:
str: A metadata value. 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 def ask_filter_path(self) -> str: # pragma: no cover
"""Ask the user for the path to the filter file. """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:", "Regex to filter the notes being processed by their path:",
only_directories=False, only_directories=False,
validate=self._validate_valid_vault_regex, validate=self._validate_valid_vault_regex,
style=self.style,
qmark="INPUT |",
).ask() ).ask()
if filter_path_regex is None: if filter_path_regex is None:
raise typer.Exit(code=1) raise typer.Exit(code=1)
@@ -331,6 +343,8 @@ class Questions:
return questionary.text( return questionary.text(
question, question,
validate=self._validate_value_exists_regex, validate=self._validate_value_exists_regex,
style=self.style,
qmark="INPUT |",
).ask() ).ask()
def ask_application_main(self) -> str: # pragma: no cover def ask_application_main(self) -> str: # pragma: no cover
@@ -345,7 +359,7 @@ class Questions:
return questionary.select( return questionary.select(
"What do you want to do?", "What do you want to do?",
choices=[ choices=[
{"name": "Vault Actions, ", "value": "vault_actions"}, {"name": "Vault Actions", "value": "vault_actions"},
{"name": "Inspect Metadata", "value": "inspect_metadata"}, {"name": "Inspect Metadata", "value": "inspect_metadata"},
{"name": "Filter Notes in Scope", "value": "filter_notes"}, {"name": "Filter Notes in Scope", "value": "filter_notes"},
{"name": "Add Metadata", "value": "add_metadata"}, {"name": "Add Metadata", "value": "add_metadata"},
@@ -359,6 +373,7 @@ class Questions:
], ],
use_shortcuts=False, use_shortcuts=False,
style=self.style, style=self.style,
qmark="INPUT |",
).ask() ).ask()
def ask_new_key(self, question: str = "New key name") -> str: # pragma: no cover 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. str: A new metadata key.
""" """
return questionary.text( return questionary.text(
question, question, validate=self._validate_new_key, style=self.style, qmark="INPUT |"
validate=self._validate_new_key,
).ask() ).ask()
def ask_new_tag(self, question: str = "New tag name") -> str: # pragma: no cover def ask_new_tag(self, question: str = "New tag name") -> str: # pragma: no cover
"""Ask the user for a new inline tag.""" """Ask the user for a new inline tag."""
return questionary.text( return questionary.text(
question, question, validate=self._validate_new_tag, style=self.style, qmark="INPUT |"
validate=self._validate_new_tag,
).ask() ).ask()
def ask_new_value(self, question: str = "New value") -> str: # pragma: no cover def ask_new_value(self, question: str = "New value") -> str: # pragma: no cover
@@ -392,8 +405,7 @@ class Questions:
str: A new metadata value. str: A new metadata value.
""" """
return questionary.text( return questionary.text(
question, question, validate=self._validate_new_value, style=self.style, qmark="INPUT |"
validate=self._validate_new_value,
).ask() ).ask()
def ask_selection( def ask_selection(
@@ -413,4 +425,5 @@ class Questions:
choices=choices, choices=choices,
use_shortcuts=False, use_shortcuts=False,
style=self.style, style=self.style,
qmark="INPUT |",
).ask() ).ask()

View File

@@ -5,6 +5,7 @@ import shutil
from pathlib import Path from pathlib import Path
import rich.repr import rich.repr
from rich import box
from rich.console import Console from rich.console import Console
from rich.progress import Progress, SpinnerColumn, TextColumn from rich.progress import Progress, SpinnerColumn, TextColumn
from rich.prompt import Confirm from rich.prompt import Confirm
@@ -124,6 +125,7 @@ class Vault:
log.debug("Backing up vault") log.debug("Backing up vault")
if self.dry_run: if self.dry_run:
alerts.dryrun(f"Backup up vault to: {self.backup_path}") alerts.dryrun(f"Backup up vault to: {self.backup_path}")
print("\n")
return return
try: try:
@@ -252,8 +254,10 @@ class Vault:
def list_editable_notes(self) -> None: def list_editable_notes(self) -> None:
"""Print a list of notes within the scope that are being edited.""" """Print a list of notes within the scope that are being edited."""
for _note in self.notes: table = Table(title="Notes in current scope", show_header=False, box=box.HORIZONTALS)
print(_note.note_path.relative_to(self.vault_path)) 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: def num_excluded_notes(self) -> int:
"""Count number of excluded notes.""" """Count number of excluded notes."""

View File

@@ -160,6 +160,19 @@ def test_info(test_vault, capsys):
assert captured.out == Regex(r"Backup +\│ None") 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: def test_contains_inline_tag(test_vault) -> None:
"""Test if the vault contains an inline tag.""" """Test if the vault contains an inline tag."""
vault_path = test_vault vault_path = test_vault