bearcli 1.3.0__tar.gz → 1.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {bearcli-1.3.0 → bearcli-1.5.0}/PKG-INFO +14 -2
- {bearcli-1.3.0 → bearcli-1.5.0}/README.md +13 -1
- {bearcli-1.3.0 → bearcli-1.5.0}/pyproject.toml +1 -1
- bearcli-1.5.0/src/bearcli/cli/__init__.py +25 -0
- bearcli-1.5.0/src/bearcli/cli/common.py +112 -0
- bearcli-1.5.0/src/bearcli/cli/export.py +118 -0
- bearcli-1.5.0/src/bearcli/cli/misc.py +105 -0
- bearcli-1.5.0/src/bearcli/cli/notes.py +502 -0
- bearcli-1.5.0/src/bearcli/cli/tags.py +104 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/db.py +11 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/markdown.py +21 -1
- bearcli-1.5.0/src/bearcli/ops.py +98 -0
- bearcli-1.5.0/src/bearcli/tui.py +843 -0
- bearcli-1.3.0/src/bearcli/cli.py +0 -933
- bearcli-1.3.0/src/bearcli/tui.py +0 -83
- {bearcli-1.3.0 → bearcli-1.5.0}/LICENSE +0 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/__init__.py +0 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/actions.py +0 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/export.py +0 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/gitsync.py +0 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/search.py +0 -0
- {bearcli-1.3.0 → bearcli-1.5.0}/src/bearcli/secrets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: bearcli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: The missing CLI for Bear notes — read, search, export, and manage your notes from the terminal
|
|
5
5
|
Keywords: bear,notes,cli,markdown,macos
|
|
6
6
|
Author: Michel Tricot
|
|
@@ -58,6 +58,7 @@ bearcli search "quarterly report" # search titles, tags, and content
|
|
|
58
58
|
bearcli get <note-id> # print a note's markdown
|
|
59
59
|
bearcli create "Idea" --tag inbox # create a note
|
|
60
60
|
bearcli export ~/bear-backup # export everything as markdown folders
|
|
61
|
+
bearcli ui # full Bear client in the terminal
|
|
61
62
|
```
|
|
62
63
|
|
|
63
64
|
## Commands
|
|
@@ -80,10 +81,21 @@ bearcli get C44D09DC-... --meta # with YAML-style frontmatter
|
|
|
80
81
|
bearcli get C44D09DC-... -r # rewrite attachment refs to absolute paths
|
|
81
82
|
bearcli get C44D09DC-... --redact-secrets # secrets replaced by placeholders
|
|
82
83
|
bearcli open C44D09DC-... # open in the Bear app
|
|
83
|
-
bearcli
|
|
84
|
+
bearcli ui # Bear in the terminal: search, edit, tag
|
|
84
85
|
bearcli stats # counts, words, top tags, notes per year
|
|
85
86
|
```
|
|
86
87
|
|
|
88
|
+
### Terminal UI
|
|
89
|
+
|
|
90
|
+
`bearcli ui` is a full Bear client in the terminal: the note list with live
|
|
91
|
+
filtering on the left, markdown preview and inline editor on the right.
|
|
92
|
+
`/` search - `enter`/`e` edit (`ctrl+s` saves, with an unsaved-changes
|
|
93
|
+
guard) - `n`/`c` new note - `t`/`T` tag/untag - `o` open in Bear - `a`
|
|
94
|
+
archive - `d` trash - `1`/`2`/`3` notes/archive/trash views - `j`/`k`
|
|
95
|
+
navigation - `?` shows the full key map. Notes with detected secrets get a
|
|
96
|
+
red title, a 🚨 badge, and the secret values highlighted in the preview and
|
|
97
|
+
editor; encrypted notes show 🔒 and keep their content in Bear.
|
|
98
|
+
|
|
87
99
|
### Search
|
|
88
100
|
|
|
89
101
|
```sh
|
|
@@ -34,6 +34,7 @@ bearcli search "quarterly report" # search titles, tags, and content
|
|
|
34
34
|
bearcli get <note-id> # print a note's markdown
|
|
35
35
|
bearcli create "Idea" --tag inbox # create a note
|
|
36
36
|
bearcli export ~/bear-backup # export everything as markdown folders
|
|
37
|
+
bearcli ui # full Bear client in the terminal
|
|
37
38
|
```
|
|
38
39
|
|
|
39
40
|
## Commands
|
|
@@ -56,10 +57,21 @@ bearcli get C44D09DC-... --meta # with YAML-style frontmatter
|
|
|
56
57
|
bearcli get C44D09DC-... -r # rewrite attachment refs to absolute paths
|
|
57
58
|
bearcli get C44D09DC-... --redact-secrets # secrets replaced by placeholders
|
|
58
59
|
bearcli open C44D09DC-... # open in the Bear app
|
|
59
|
-
bearcli
|
|
60
|
+
bearcli ui # Bear in the terminal: search, edit, tag
|
|
60
61
|
bearcli stats # counts, words, top tags, notes per year
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### Terminal UI
|
|
65
|
+
|
|
66
|
+
`bearcli ui` is a full Bear client in the terminal: the note list with live
|
|
67
|
+
filtering on the left, markdown preview and inline editor on the right.
|
|
68
|
+
`/` search - `enter`/`e` edit (`ctrl+s` saves, with an unsaved-changes
|
|
69
|
+
guard) - `n`/`c` new note - `t`/`T` tag/untag - `o` open in Bear - `a`
|
|
70
|
+
archive - `d` trash - `1`/`2`/`3` notes/archive/trash views - `j`/`k`
|
|
71
|
+
navigation - `?` shows the full key map. Notes with detected secrets get a
|
|
72
|
+
red title, a 🚨 badge, and the secret values highlighted in the preview and
|
|
73
|
+
editor; encrypted notes show 🔒 and keep their content in Bear.
|
|
74
|
+
|
|
63
75
|
### Search
|
|
64
76
|
|
|
65
77
|
```sh
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Typer CLI for reading notes from the Bear note app."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
|
|
5
|
+
from bearcli.cli import export, misc, notes, tags # noqa: F401 # command registration
|
|
6
|
+
from bearcli.cli.common import app
|
|
7
|
+
from bearcli.cli.notes import create, get, list_notes, open_note, search
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _alias(name: str, target: str, func: Callable) -> None:
|
|
11
|
+
summary = (func.__doc__ or "").strip().splitlines()[0]
|
|
12
|
+
app.command(name, help=f"{summary} (alias for `bearcli {target}`)", rich_help_panel="Shortcuts")(func)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
_alias("list", "note list", list_notes)
|
|
16
|
+
|
|
17
|
+
_alias("search", "note search", search)
|
|
18
|
+
|
|
19
|
+
_alias("get", "note get", get)
|
|
20
|
+
|
|
21
|
+
_alias("open", "note open", open_note)
|
|
22
|
+
|
|
23
|
+
_alias("create", "note create", create)
|
|
24
|
+
|
|
25
|
+
__all__ = ["app"]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Shared Typer apps, output types, and helpers for the CLI commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
|
|
15
|
+
from bearcli import actions
|
|
16
|
+
from bearcli.db import AmbiguousNoteId, BearDB, Note, note_metadata
|
|
17
|
+
|
|
18
|
+
app = typer.Typer(help="Read notes from the Bear note app.", no_args_is_help=True, add_completion=False)
|
|
19
|
+
|
|
20
|
+
note_app = typer.Typer(help="Create, read, and modify notes.", no_args_is_help=True)
|
|
21
|
+
|
|
22
|
+
tag_app = typer.Typer(help="List and manage tags.", no_args_is_help=True)
|
|
23
|
+
|
|
24
|
+
app.add_typer(note_app, name="note")
|
|
25
|
+
|
|
26
|
+
app.add_typer(tag_app, name="tag")
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class OutputFormat(StrEnum):
|
|
32
|
+
table = "table"
|
|
33
|
+
json = "json"
|
|
34
|
+
text = "text"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class OnlyFilter(StrEnum):
|
|
38
|
+
pinned = "pinned"
|
|
39
|
+
encrypted = "encrypted"
|
|
40
|
+
trashed = "trashed"
|
|
41
|
+
archived = "archived"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _note_to_dict(note: Note, with_text: bool = False) -> dict:
|
|
45
|
+
data = note_metadata(note)
|
|
46
|
+
if with_text:
|
|
47
|
+
data["text"] = note.text
|
|
48
|
+
data["attachments"] = [
|
|
49
|
+
{
|
|
50
|
+
"filename": a.filename,
|
|
51
|
+
"path": str(a.path),
|
|
52
|
+
"size": a.size,
|
|
53
|
+
"exists": a.exists,
|
|
54
|
+
}
|
|
55
|
+
for a in note.attachments
|
|
56
|
+
]
|
|
57
|
+
return data
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
DbPathOption = Annotated[
|
|
61
|
+
Path,
|
|
62
|
+
typer.Option("--db", envvar="BEAR_DB_PATH", help="Path to the Bear SQLite database."),
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _open_db(path: Path) -> BearDB:
|
|
67
|
+
try:
|
|
68
|
+
return BearDB(path)
|
|
69
|
+
except FileNotFoundError as exc:
|
|
70
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
71
|
+
raise typer.Exit(1) from None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _parse_date(value: str | None, option: str) -> datetime | None:
|
|
75
|
+
if value is None:
|
|
76
|
+
return None
|
|
77
|
+
try:
|
|
78
|
+
return datetime.fromisoformat(value)
|
|
79
|
+
except ValueError:
|
|
80
|
+
console.print(
|
|
81
|
+
f"[red]Error:[/red] invalid date for {option}: {value!r} "
|
|
82
|
+
"(expected ISO format, e.g. 2026-07-01 or 2026-07-01T14:30)"
|
|
83
|
+
)
|
|
84
|
+
raise typer.Exit(2) from None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _text_or_stdin(text: str | None) -> str | None:
|
|
88
|
+
if text is None and not sys.stdin.isatty():
|
|
89
|
+
return sys.stdin.read()
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _require_note(db: BearDB, note_id: str) -> Note:
|
|
94
|
+
try:
|
|
95
|
+
note = db.get_note(note_id)
|
|
96
|
+
except AmbiguousNoteId as exc:
|
|
97
|
+
console.print(f"[red]Error:[/red] note id prefix {exc.prefix!r} matches several notes:")
|
|
98
|
+
for full_id, title in exc.matches:
|
|
99
|
+
console.print(f" {full_id} {title}")
|
|
100
|
+
raise typer.Exit(1) from None
|
|
101
|
+
if note is None:
|
|
102
|
+
console.print(f"[red]Error:[/red] no note with id {note_id!r}")
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
return note
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _verify(ok: Callable[[], bool], success: str, failure: str) -> None:
|
|
108
|
+
if actions.wait_for(ok):
|
|
109
|
+
console.print(success)
|
|
110
|
+
else:
|
|
111
|
+
console.print(f"[red]Error:[/red] {failure}")
|
|
112
|
+
raise typer.Exit(1)
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""The export command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.markup import escape as rich_escape
|
|
11
|
+
from rich.table import Table
|
|
12
|
+
|
|
13
|
+
from bearcli.cli.common import (
|
|
14
|
+
DbPathOption,
|
|
15
|
+
_open_db,
|
|
16
|
+
app,
|
|
17
|
+
console,
|
|
18
|
+
)
|
|
19
|
+
from bearcli.db import DEFAULT_DB_PATH
|
|
20
|
+
from bearcli.export import export_notes
|
|
21
|
+
from bearcli.gitsync import GitError, export_and_push
|
|
22
|
+
from bearcli.secrets import SecretFinding, redaction_map, scan_notes
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _report_secrets(findings: list[SecretFinding]) -> None:
|
|
26
|
+
table = Table(box=box.ROUNDED, header_style="bold")
|
|
27
|
+
table.add_column("Note", overflow="ellipsis", max_width=30)
|
|
28
|
+
table.add_column("ID", style="dim", no_wrap=True)
|
|
29
|
+
table.add_column("Rule", style="yellow")
|
|
30
|
+
table.add_column("Line", justify="right")
|
|
31
|
+
table.add_column("Match", style="red")
|
|
32
|
+
for f in findings:
|
|
33
|
+
table.add_row(f.note_title, f.note_id, f.rule, str(f.line), f.excerpt)
|
|
34
|
+
console.print(table)
|
|
35
|
+
notes = len({f.note_id for f in findings})
|
|
36
|
+
console.print(
|
|
37
|
+
f"[red]Export blocked:[/red] {len(findings)} potential secret(s) in {notes} note(s). "
|
|
38
|
+
"Move them somewhere safe (or into an encrypted note), re-run with --redact-secrets "
|
|
39
|
+
"to export with placeholders, or with --allow-secrets to export as-is."
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.command()
|
|
44
|
+
def export(
|
|
45
|
+
dest: Annotated[Path, typer.Argument(help="Destination directory for the markdown files.")],
|
|
46
|
+
sync: Annotated[
|
|
47
|
+
bool,
|
|
48
|
+
typer.Option(
|
|
49
|
+
"--sync",
|
|
50
|
+
help="Only rewrite notes that changed since the last export instead of everything.",
|
|
51
|
+
),
|
|
52
|
+
] = False,
|
|
53
|
+
push: Annotated[
|
|
54
|
+
bool,
|
|
55
|
+
typer.Option(
|
|
56
|
+
"--push",
|
|
57
|
+
help="Treat DEST as a git clone: commit the export and push. Bear is the source of "
|
|
58
|
+
"truth — remote or manual edits are kept in history but overwritten in HEAD.",
|
|
59
|
+
),
|
|
60
|
+
] = False,
|
|
61
|
+
allow_secrets: Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
typer.Option("--allow-secrets", help="Export even if the secret scan finds potential credentials."),
|
|
64
|
+
] = False,
|
|
65
|
+
redact_secrets: Annotated[
|
|
66
|
+
bool,
|
|
67
|
+
typer.Option(
|
|
68
|
+
"--redact-secrets",
|
|
69
|
+
help="Export with detected secrets replaced by a [redacted: <rule>] placeholder "
|
|
70
|
+
"(notes in Bear are untouched).",
|
|
71
|
+
),
|
|
72
|
+
] = False,
|
|
73
|
+
db_path: DbPathOption = DEFAULT_DB_PATH,
|
|
74
|
+
) -> None:
|
|
75
|
+
"""Export all notes as markdown files with frontmatter and attachments."""
|
|
76
|
+
if allow_secrets and redact_secrets:
|
|
77
|
+
console.print("[red]Error:[/red] --allow-secrets and --redact-secrets are mutually exclusive")
|
|
78
|
+
raise typer.Exit(2)
|
|
79
|
+
db = _open_db(db_path)
|
|
80
|
+
try:
|
|
81
|
+
redactions: dict[str, dict[str, str]] | None = None
|
|
82
|
+
if not allow_secrets:
|
|
83
|
+
with console.status("Scanning notes for secrets…", spinner="dots"):
|
|
84
|
+
candidates = db.list_notes(limit=None, include_archived=True, with_text=True)
|
|
85
|
+
findings = scan_notes(candidates)
|
|
86
|
+
if findings and not redact_secrets:
|
|
87
|
+
_report_secrets(findings)
|
|
88
|
+
raise typer.Exit(1)
|
|
89
|
+
if findings:
|
|
90
|
+
redactions = redaction_map(findings)
|
|
91
|
+
with console.status("Exporting…", spinner="dots") as status:
|
|
92
|
+
update = lambda msg: status.update(rich_escape(msg)) # noqa: E731
|
|
93
|
+
if push:
|
|
94
|
+
try:
|
|
95
|
+
result, outcome = export_and_push(db, dest, sync=sync, progress=update, redactions=redactions)
|
|
96
|
+
except GitError as exc:
|
|
97
|
+
console.print(f"[red]Error:[/red] {exc}")
|
|
98
|
+
raise typer.Exit(1) from None
|
|
99
|
+
else:
|
|
100
|
+
result = export_notes(db, dest, sync=sync, progress=update, redactions=redactions)
|
|
101
|
+
finally:
|
|
102
|
+
db.close()
|
|
103
|
+
|
|
104
|
+
parts = [f"{result.written} written"]
|
|
105
|
+
if push:
|
|
106
|
+
parts.append(outcome)
|
|
107
|
+
if sync:
|
|
108
|
+
parts.append(f"{result.unchanged} unchanged")
|
|
109
|
+
if result.removed:
|
|
110
|
+
parts.append(f"{result.removed} removed")
|
|
111
|
+
if result.skipped_encrypted:
|
|
112
|
+
parts.append(f"{result.skipped_encrypted} encrypted skipped")
|
|
113
|
+
if result.index_updated:
|
|
114
|
+
parts.append("index updated")
|
|
115
|
+
if redactions:
|
|
116
|
+
secrets_count = sum(len(v) for v in redactions.values())
|
|
117
|
+
parts.append(f"{secrets_count} secret(s) redacted in {len(redactions)} note(s)")
|
|
118
|
+
console.print(f"Exported to {dest}: " + ", ".join(parts))
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Library-wide commands: stats and the terminal UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Annotated
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from bearcli.cli.common import (
|
|
13
|
+
DbPathOption,
|
|
14
|
+
OutputFormat,
|
|
15
|
+
_open_db,
|
|
16
|
+
app,
|
|
17
|
+
console,
|
|
18
|
+
)
|
|
19
|
+
from bearcli.db import DEFAULT_DB_PATH
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@app.command()
|
|
23
|
+
def stats(
|
|
24
|
+
fmt: Annotated[
|
|
25
|
+
OutputFormat,
|
|
26
|
+
typer.Option("--format", "-f", help="Output format: table or json."),
|
|
27
|
+
] = OutputFormat.table,
|
|
28
|
+
db_path: DbPathOption = DEFAULT_DB_PATH,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Show statistics about the note library."""
|
|
31
|
+
db = _open_db(db_path)
|
|
32
|
+
try:
|
|
33
|
+
notes = db.list_notes(limit=None, include_trashed=True, include_archived=True, with_text=True)
|
|
34
|
+
tag_counts = db.list_tags()
|
|
35
|
+
attachment_count, attachment_bytes = db.attachment_stats()
|
|
36
|
+
finally:
|
|
37
|
+
db.close()
|
|
38
|
+
|
|
39
|
+
active = [n for n in notes if not n.trashed and not n.archived]
|
|
40
|
+
by_year: dict[str, int] = {}
|
|
41
|
+
for n in notes:
|
|
42
|
+
if not n.trashed and n.created:
|
|
43
|
+
year = str(n.created.year)
|
|
44
|
+
by_year[year] = by_year.get(year, 0) + 1
|
|
45
|
+
|
|
46
|
+
data = {
|
|
47
|
+
"notes": len(notes),
|
|
48
|
+
"active": len(active),
|
|
49
|
+
"archived": sum(1 for n in notes if n.archived and not n.trashed),
|
|
50
|
+
"trashed": sum(1 for n in notes if n.trashed),
|
|
51
|
+
"pinned": sum(1 for n in notes if n.pinned and not n.trashed),
|
|
52
|
+
"encrypted": sum(1 for n in notes if n.encrypted and not n.trashed),
|
|
53
|
+
"words": sum(len((n.text or "").split()) for n in notes if not n.trashed),
|
|
54
|
+
"tags": len(tag_counts),
|
|
55
|
+
"attachments": attachment_count,
|
|
56
|
+
"attachment_bytes": attachment_bytes,
|
|
57
|
+
"notes_by_year": dict(sorted(by_year.items())),
|
|
58
|
+
"top_tags": [{"tag": t, "notes": c} for t, c in sorted(tag_counts, key=lambda tc: -tc[1])[:10]],
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if fmt is OutputFormat.json:
|
|
62
|
+
print(json.dumps(data, indent=2, ensure_ascii=False))
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
table = Table(box=box.ROUNDED, show_header=False, width=44)
|
|
66
|
+
table.add_column(style="bold")
|
|
67
|
+
table.add_column(justify="right")
|
|
68
|
+
table.add_row("Notes", str(data["notes"]))
|
|
69
|
+
table.add_row(" active", str(data["active"]))
|
|
70
|
+
table.add_row(" archived", str(data["archived"]))
|
|
71
|
+
table.add_row(" trashed", str(data["trashed"]))
|
|
72
|
+
table.add_row(" pinned", str(data["pinned"]))
|
|
73
|
+
table.add_row(" encrypted", str(data["encrypted"]))
|
|
74
|
+
table.add_row("Words", f"{data['words']:,}")
|
|
75
|
+
table.add_row("Tags", str(data["tags"]))
|
|
76
|
+
table.add_row("Attachments", f"{attachment_count} ({attachment_bytes / 1_000_000:.1f} MB)")
|
|
77
|
+
console.print(table)
|
|
78
|
+
|
|
79
|
+
if data["top_tags"]:
|
|
80
|
+
tag_table = Table(box=box.ROUNDED, header_style="bold", width=44)
|
|
81
|
+
tag_table.add_column("Top tags", style="cyan")
|
|
82
|
+
tag_table.add_column("Notes", justify="right")
|
|
83
|
+
for entry in data["top_tags"]:
|
|
84
|
+
tag_table.add_row(str(entry["tag"]), str(entry["notes"]))
|
|
85
|
+
console.print(tag_table)
|
|
86
|
+
|
|
87
|
+
year_table = Table(box=box.ROUNDED, header_style="bold", width=44)
|
|
88
|
+
year_table.add_column("Created", style="cyan")
|
|
89
|
+
year_table.add_column("Notes", justify="right")
|
|
90
|
+
for year, count in data["notes_by_year"].items():
|
|
91
|
+
year_table.add_row(year, str(count))
|
|
92
|
+
console.print(year_table)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app.command()
|
|
96
|
+
def ui(
|
|
97
|
+
fuzzy: Annotated[bool, typer.Option("--fuzzy", help="Typo-tolerant ranked filtering.")] = False,
|
|
98
|
+
tag_filter: Annotated[str | None, typer.Option("--tag", "-t", help="Restrict to notes with this tag.")] = None,
|
|
99
|
+
db_path: DbPathOption = DEFAULT_DB_PATH,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Bear in the terminal: search, edit, create, tag, and organize notes."""
|
|
102
|
+
from bearcli.tui import run_ui
|
|
103
|
+
|
|
104
|
+
_open_db(db_path).close() # fail fast with a clear message if the db is missing
|
|
105
|
+
run_ui(fuzzy=fuzzy, db_path=db_path, tag_filter=tag_filter)
|