bearcli 1.4.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.
Potentially problematic release.
This version of bearcli might be problematic. Click here for more details.
- {bearcli-1.4.0 → bearcli-1.5.0}/PKG-INFO +8 -6
- {bearcli-1.4.0 → bearcli-1.5.0}/README.md +7 -5
- {bearcli-1.4.0 → bearcli-1.5.0}/pyproject.toml +1 -1
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/cli/misc.py +2 -6
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/tui.py +244 -24
- {bearcli-1.4.0 → bearcli-1.5.0}/LICENSE +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/__init__.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/actions.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/cli/__init__.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/cli/common.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/cli/export.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/cli/notes.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/cli/tags.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/db.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/export.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/gitsync.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/markdown.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/ops.py +0 -0
- {bearcli-1.4.0 → bearcli-1.5.0}/src/bearcli/search.py +0 -0
- {bearcli-1.4.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
|
|
@@ -88,11 +88,13 @@ bearcli stats # counts, words, top tags, notes pe
|
|
|
88
88
|
### Terminal UI
|
|
89
89
|
|
|
90
90
|
`bearcli ui` is a full Bear client in the terminal: the note list with live
|
|
91
|
-
filtering on the left, preview and inline
|
|
92
|
-
`/` search - `
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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.
|
|
96
98
|
|
|
97
99
|
### Search
|
|
98
100
|
|
|
@@ -64,11 +64,13 @@ bearcli stats # counts, words, top tags, notes pe
|
|
|
64
64
|
### Terminal UI
|
|
65
65
|
|
|
66
66
|
`bearcli ui` is a full Bear client in the terminal: the note list with live
|
|
67
|
-
filtering on the left, preview and inline
|
|
68
|
-
`/` search - `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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.
|
|
72
74
|
|
|
73
75
|
### Search
|
|
74
76
|
|
|
@@ -101,9 +101,5 @@ def ui(
|
|
|
101
101
|
"""Bear in the terminal: search, edit, create, tag, and organize notes."""
|
|
102
102
|
from bearcli.tui import run_ui
|
|
103
103
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
notes = db.list_notes(limit=None, tag=tag_filter, with_text=True)
|
|
107
|
-
finally:
|
|
108
|
-
db.close()
|
|
109
|
-
run_ui(notes, fuzzy=fuzzy, db_path=db_path, tag_filter=tag_filter)
|
|
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)
|
|
@@ -20,7 +20,7 @@ from textual.app import App, ComposeResult
|
|
|
20
20
|
from textual.binding import Binding
|
|
21
21
|
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
22
22
|
from textual.screen import ModalScreen
|
|
23
|
-
from textual.widgets import
|
|
23
|
+
from textual.widgets import Input, Label, LoadingIndicator, OptionList, Static, TextArea
|
|
24
24
|
from textual.widgets.option_list import Option
|
|
25
25
|
|
|
26
26
|
from bearcli import actions, ops
|
|
@@ -98,6 +98,19 @@ def _highlighted(value: str, query: str, base_style: str = "") -> Text:
|
|
|
98
98
|
class SecretTextArea(TextArea):
|
|
99
99
|
"""TextArea that renders detected secret values on a light red background."""
|
|
100
100
|
|
|
101
|
+
async def _on_key(self, event: events.Key) -> None:
|
|
102
|
+
# With tab_behavior="indent", TextArea claims escape as its
|
|
103
|
+
# focus-escape key (in _on_key, not via bindings); route it to the
|
|
104
|
+
# app's editor-exit instead.
|
|
105
|
+
if event.key == "escape":
|
|
106
|
+
event.stop()
|
|
107
|
+
event.prevent_default()
|
|
108
|
+
app = self.app
|
|
109
|
+
assert isinstance(app, BearUI)
|
|
110
|
+
app.action_back_or_quit()
|
|
111
|
+
return
|
|
112
|
+
await super()._on_key(event)
|
|
113
|
+
|
|
101
114
|
def __init__(self, *args, **kwargs):
|
|
102
115
|
super().__init__(*args, **kwargs)
|
|
103
116
|
self.secret_values: list[str] = []
|
|
@@ -124,6 +137,32 @@ class _TagSuggestions(OptionList):
|
|
|
124
137
|
super().action_cursor_down()
|
|
125
138
|
|
|
126
139
|
|
|
140
|
+
class ConfirmDiscard(ModalScreen[str | None]):
|
|
141
|
+
"""Confirmation shown when leaving the editor with unsaved changes."""
|
|
142
|
+
|
|
143
|
+
BINDINGS = [
|
|
144
|
+
Binding("s", "answer('save')", "Save"),
|
|
145
|
+
Binding("y", "answer('discard')", "Discard"),
|
|
146
|
+
Binding("n,escape", "answer('keep')", "Keep editing"),
|
|
147
|
+
]
|
|
148
|
+
CSS = """
|
|
149
|
+
ConfirmDiscard { align: center middle; }
|
|
150
|
+
#confirm-box {
|
|
151
|
+
width: 52; height: auto; padding: 1 2;
|
|
152
|
+
border: round $warning; background: $panel;
|
|
153
|
+
}
|
|
154
|
+
#confirm-keys { color: $text-muted; margin-top: 1; }
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
def compose(self) -> ComposeResult:
|
|
158
|
+
with Vertical(id="confirm-box"):
|
|
159
|
+
yield Label("You have unsaved changes.")
|
|
160
|
+
yield Label("[b]s[/b] save [b]y[/b] discard [b]n[/b]/[b]esc[/b] keep editing", id="confirm-keys")
|
|
161
|
+
|
|
162
|
+
def action_answer(self, answer: str) -> None:
|
|
163
|
+
self.dismiss(answer)
|
|
164
|
+
|
|
165
|
+
|
|
127
166
|
class TagScreen(ModalScreen[str | None]):
|
|
128
167
|
"""Tag prompt with autocompletion over known tags."""
|
|
129
168
|
|
|
@@ -179,6 +218,97 @@ class TagScreen(ModalScreen[str | None]):
|
|
|
179
218
|
self.dismiss(None)
|
|
180
219
|
|
|
181
220
|
|
|
221
|
+
BROWSE_KEYS_BEAR = [
|
|
222
|
+
("enter/e", "Edit"),
|
|
223
|
+
("n/c", "New"),
|
|
224
|
+
("t", "Tag"),
|
|
225
|
+
("o", "Open in Bear"),
|
|
226
|
+
("a", "Archive"),
|
|
227
|
+
("d", "Trash"),
|
|
228
|
+
]
|
|
229
|
+
BROWSE_KEYS_TUI = [
|
|
230
|
+
("/", "Search"),
|
|
231
|
+
("1/2/3", "View"),
|
|
232
|
+
("tab", "Pane"),
|
|
233
|
+
("?", "Help"),
|
|
234
|
+
("esc", "Quit"),
|
|
235
|
+
]
|
|
236
|
+
EDIT_KEYS_BEAR = [("ctrl+s", "Save to Bear")]
|
|
237
|
+
EDIT_KEYS_TUI = [("tab", "Indent"), ("esc", "Discard")]
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _keybar_markup(keys: list[tuple[str, str]]) -> str:
|
|
241
|
+
return " ".join(f"[bold]{key}[/bold] [dim]{label}[/dim]" for key, label in keys)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class KeyBar(Horizontal):
|
|
245
|
+
"""Bottom key bar: Bear actions on the left, TUI controls on the right."""
|
|
246
|
+
|
|
247
|
+
DEFAULT_CSS = """
|
|
248
|
+
KeyBar { dock: bottom; height: 1; background: $panel; padding: 0 1; }
|
|
249
|
+
KeyBar > #kb-left { width: auto; }
|
|
250
|
+
KeyBar > #kb-right { width: 1fr; text-align: right; }
|
|
251
|
+
"""
|
|
252
|
+
|
|
253
|
+
def compose(self) -> ComposeResult:
|
|
254
|
+
yield Static(id="kb-left")
|
|
255
|
+
yield Static(id="kb-right")
|
|
256
|
+
|
|
257
|
+
def show(self, bear: list[tuple[str, str]], tui: list[tuple[str, str]]) -> None:
|
|
258
|
+
self.query_one("#kb-left", Static).update(_keybar_markup(bear))
|
|
259
|
+
self.query_one("#kb-right", Static).update(_keybar_markup(tui))
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
HELP_ROWS = [
|
|
263
|
+
("Navigate", ""),
|
|
264
|
+
("↑ or k", "previous note"),
|
|
265
|
+
("↓ or j", "next note"),
|
|
266
|
+
("/", "focus search"),
|
|
267
|
+
("tab", "switch pane"),
|
|
268
|
+
("1 / 2 / 3", "notes / archive / trash view"),
|
|
269
|
+
("Act on the selected note", ""),
|
|
270
|
+
("enter or e", "edit in the right panel"),
|
|
271
|
+
("n or c", "new note"),
|
|
272
|
+
("t / T", "add / remove a tag"),
|
|
273
|
+
("o", "open in Bear"),
|
|
274
|
+
("a", "archive"),
|
|
275
|
+
("d", "move to trash"),
|
|
276
|
+
("r", "reload from Bear"),
|
|
277
|
+
("While editing", ""),
|
|
278
|
+
("ctrl+s", "save to Bear"),
|
|
279
|
+
("esc", "discard changes"),
|
|
280
|
+
]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class HelpScreen(ModalScreen[None]):
|
|
284
|
+
"""Key map overlay."""
|
|
285
|
+
|
|
286
|
+
BINDINGS = [Binding("escape,question_mark", "close", "Close")]
|
|
287
|
+
CSS = """
|
|
288
|
+
HelpScreen { align: center middle; }
|
|
289
|
+
#help-box {
|
|
290
|
+
width: 62; height: auto; padding: 1 2;
|
|
291
|
+
border: round $accent; background: $panel;
|
|
292
|
+
}
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
def compose(self) -> ComposeResult:
|
|
296
|
+
table = RichTable.grid(padding=(0, 2))
|
|
297
|
+
table.add_column(style="bold", no_wrap=True)
|
|
298
|
+
table.add_column()
|
|
299
|
+
for key, description in HELP_ROWS:
|
|
300
|
+
if not description:
|
|
301
|
+
if table.row_count:
|
|
302
|
+
table.add_row("", "") # blank line between sections
|
|
303
|
+
table.add_row(Text(key, "dim italic"), "")
|
|
304
|
+
else:
|
|
305
|
+
table.add_row(key, description)
|
|
306
|
+
yield Static(table, id="help-box")
|
|
307
|
+
|
|
308
|
+
def action_close(self) -> None:
|
|
309
|
+
self.dismiss(None)
|
|
310
|
+
|
|
311
|
+
|
|
182
312
|
class BearUI(App):
|
|
183
313
|
"""Browse, edit, and organize Bear notes from the terminal."""
|
|
184
314
|
|
|
@@ -188,17 +318,19 @@ class BearUI(App):
|
|
|
188
318
|
CSS = """
|
|
189
319
|
#query { dock: top; margin: 0 1; border: round $primary; }
|
|
190
320
|
#query:focus { border: round $accent; }
|
|
191
|
-
#
|
|
192
|
-
#
|
|
321
|
+
#list-pane { width: 34%; border: round $panel-lighten-2; }
|
|
322
|
+
#list-pane:focus-within { border: round $accent; }
|
|
323
|
+
#results { height: 1fr; border: none; background: transparent; }
|
|
324
|
+
#list-pane LoadingIndicator { height: 1fr; }
|
|
193
325
|
#side { width: 66%; }
|
|
194
326
|
#preview-pane { height: 1fr; border: round $panel-lighten-2; padding: 0 1; }
|
|
327
|
+
#preview-pane:focus { border: round $accent; }
|
|
195
328
|
#editor { height: 1fr; border: round $accent; display: none; }
|
|
196
|
-
|
|
329
|
+
#main { height: 1fr; margin: 0 1; }
|
|
197
330
|
"""
|
|
198
331
|
|
|
199
332
|
BINDINGS = [
|
|
200
|
-
|
|
201
|
-
Binding("/", "focus_search", "Search"),
|
|
333
|
+
# Bear actions (left of the footer)
|
|
202
334
|
Binding("e", "edit_selected", "Edit", key_display="enter/e"),
|
|
203
335
|
Binding("enter", "edit_selected", "Edit", show=False),
|
|
204
336
|
Binding("n", "new_note", "New", key_display="n/c"),
|
|
@@ -206,25 +338,38 @@ class BearUI(App):
|
|
|
206
338
|
Binding("t", "add_tag", "Tag"),
|
|
207
339
|
Binding("T", "remove_tag", "Untag", show=False),
|
|
208
340
|
Binding("o", "open_in_bear", "Open in Bear"),
|
|
209
|
-
Binding("r", "refresh", "Refresh", show=False),
|
|
210
341
|
Binding("a", "archive_note", "Archive"),
|
|
211
342
|
Binding("d", "trash_note", "Trash"),
|
|
343
|
+
Binding("r", "refresh", "Refresh", show=False),
|
|
344
|
+
Binding("j", "cursor_down", "Next note", show=False),
|
|
345
|
+
Binding("k", "cursor_up", "Previous note", show=False),
|
|
212
346
|
Binding("ctrl+s", "save_edit", "Save to Bear"),
|
|
213
|
-
|
|
347
|
+
# TUI controls (right of the footer)
|
|
348
|
+
Binding("/", "focus_search", "Search"),
|
|
349
|
+
Binding("1", "switch_view('notes')", "View", key_display="1/2/3"),
|
|
350
|
+
Binding("2", "switch_view('archive')", "Archive view", show=False),
|
|
351
|
+
Binding("3", "switch_view('trash')", "Trash view", show=False),
|
|
352
|
+
Binding("tab", "focus_next", "Switch pane", priority=True),
|
|
353
|
+
Binding("question_mark", "help", "Help", key_display="?"),
|
|
354
|
+
Binding("escape", "back_or_quit", "Quit"),
|
|
214
355
|
]
|
|
215
356
|
|
|
216
357
|
def __init__(
|
|
217
358
|
self,
|
|
218
|
-
notes: list[Note],
|
|
359
|
+
notes: list[Note] | None = None,
|
|
219
360
|
fuzzy: bool = False,
|
|
220
361
|
db_path: Path = DEFAULT_DB_PATH,
|
|
221
362
|
tag_filter: str | None = None,
|
|
222
363
|
):
|
|
223
364
|
super().__init__()
|
|
224
|
-
self.
|
|
365
|
+
self._preloaded = notes is not None
|
|
366
|
+
self._focus_list_after_load = False
|
|
367
|
+
self._editor_original = ""
|
|
368
|
+
self.notes = notes or []
|
|
225
369
|
self.fuzzy = fuzzy
|
|
226
370
|
self.db_path = db_path
|
|
227
371
|
self.tag_filter = tag_filter
|
|
372
|
+
self.view = "notes" # notes | archive | trash
|
|
228
373
|
self.search_query = ""
|
|
229
374
|
self.shown: list[Note] = []
|
|
230
375
|
self.editing: Note | None = None
|
|
@@ -237,13 +382,14 @@ class BearUI(App):
|
|
|
237
382
|
|
|
238
383
|
def compose(self) -> ComposeResult:
|
|
239
384
|
yield Input(placeholder="Search notes…", id="query")
|
|
240
|
-
with Horizontal():
|
|
241
|
-
|
|
385
|
+
with Horizontal(id="main"):
|
|
386
|
+
with Vertical(id="list-pane"):
|
|
387
|
+
yield OptionList(id="results")
|
|
242
388
|
with Vertical(id="side"):
|
|
243
389
|
with VerticalScroll(id="preview-pane"):
|
|
244
390
|
yield Static(id="preview-content")
|
|
245
|
-
yield SecretTextArea(language="markdown", id="editor")
|
|
246
|
-
yield
|
|
391
|
+
yield SecretTextArea(language="markdown", id="editor", tab_behavior="indent")
|
|
392
|
+
yield KeyBar()
|
|
247
393
|
|
|
248
394
|
@property
|
|
249
395
|
def edit_mode(self) -> bool:
|
|
@@ -260,6 +406,10 @@ class BearUI(App):
|
|
|
260
406
|
"trash_note",
|
|
261
407
|
"focus_search",
|
|
262
408
|
"refresh",
|
|
409
|
+
"switch_view",
|
|
410
|
+
"focus_next",
|
|
411
|
+
"cursor_down",
|
|
412
|
+
"cursor_up",
|
|
263
413
|
}
|
|
264
414
|
if self.edit_mode and action in browse_only:
|
|
265
415
|
return False
|
|
@@ -270,9 +420,27 @@ class BearUI(App):
|
|
|
270
420
|
# ── searching / listing ──────────────────────────────────────────────
|
|
271
421
|
|
|
272
422
|
def on_mount(self) -> None:
|
|
273
|
-
self.
|
|
423
|
+
self.query_one(KeyBar).show(BROWSE_KEYS_BEAR, BROWSE_KEYS_TUI)
|
|
274
424
|
self.query_one("#results", OptionList).focus()
|
|
275
|
-
self.
|
|
425
|
+
if self._preloaded:
|
|
426
|
+
self._show_results("", [SearchResult(note=n, snippet="") for n in self.notes])
|
|
427
|
+
self._scan_secrets()
|
|
428
|
+
else:
|
|
429
|
+
self._set_list_loading(True)
|
|
430
|
+
self._rehydrate()
|
|
431
|
+
|
|
432
|
+
def _set_list_loading(self, on: bool) -> None:
|
|
433
|
+
results = self.query_one("#results", OptionList)
|
|
434
|
+
pane = self.query_one("#list-pane")
|
|
435
|
+
if on:
|
|
436
|
+
self._focus_list_after_load = not (isinstance(self.focused, Input) and self.focused.value)
|
|
437
|
+
results.display = False
|
|
438
|
+
if not pane.query("LoadingIndicator"):
|
|
439
|
+
pane.mount(LoadingIndicator())
|
|
440
|
+
else:
|
|
441
|
+
for indicator in pane.query(LoadingIndicator):
|
|
442
|
+
indicator.remove()
|
|
443
|
+
results.display = True
|
|
276
444
|
|
|
277
445
|
@work(exclusive=True, thread=True, group="scan")
|
|
278
446
|
def _scan_secrets(self) -> None:
|
|
@@ -280,6 +448,14 @@ class BearUI(App):
|
|
|
280
448
|
|
|
281
449
|
def _apply_secret_values(self, values: dict[str, dict[str, str]]) -> None:
|
|
282
450
|
self.secret_values = values
|
|
451
|
+
results = self.query_one("#results", OptionList)
|
|
452
|
+
self._set_list_loading(False)
|
|
453
|
+
if self._focus_list_after_load:
|
|
454
|
+
# The hidden list dropped focus into the search box during the
|
|
455
|
+
# load; take it back unless the user started typing.
|
|
456
|
+
self._focus_list_after_load = False
|
|
457
|
+
if not self.query_one("#query", Input).value:
|
|
458
|
+
results.focus()
|
|
283
459
|
self._run_filter(self.search_query)
|
|
284
460
|
|
|
285
461
|
@work(thread=True, group="refresh")
|
|
@@ -290,7 +466,7 @@ class BearUI(App):
|
|
|
290
466
|
fresh = db.get_note(note_id)
|
|
291
467
|
finally:
|
|
292
468
|
db.close()
|
|
293
|
-
if fresh is None or
|
|
469
|
+
if fresh is None or not self._in_current_view(fresh):
|
|
294
470
|
self.notes = [n for n in self.notes if n.id != note_id]
|
|
295
471
|
self.secret_values.pop(note_id, None)
|
|
296
472
|
else:
|
|
@@ -307,10 +483,11 @@ class BearUI(App):
|
|
|
307
483
|
|
|
308
484
|
@work(exclusive=True, thread=True, group="rehydrate")
|
|
309
485
|
def _rehydrate(self) -> None:
|
|
310
|
-
"""Reload
|
|
486
|
+
"""Reload the current view from the database - also catches edits made in Bear."""
|
|
311
487
|
db = BearDB(self.db_path)
|
|
312
488
|
try:
|
|
313
|
-
|
|
489
|
+
only = {"archive": "archived", "trash": "trashed"}.get(self.view)
|
|
490
|
+
notes = db.list_notes(limit=None, tag=self.tag_filter, with_text=True, only=only)
|
|
314
491
|
finally:
|
|
315
492
|
db.close()
|
|
316
493
|
self.notes = notes
|
|
@@ -365,7 +542,8 @@ class BearUI(App):
|
|
|
365
542
|
result_list = self.query_one("#results", OptionList)
|
|
366
543
|
result_list.clear_options()
|
|
367
544
|
result_list.add_options(options)
|
|
368
|
-
|
|
545
|
+
prefix = {"notes": "", "archive": "Archive · ", "trash": "Trash · "}[self.view]
|
|
546
|
+
self.query_one("#list-pane").border_title = f"{prefix}{len(self.shown)} / {len(self.notes)} notes"
|
|
369
547
|
select_id, self._select_id = self._select_id, None
|
|
370
548
|
index = next((i for i, n in enumerate(self.shown) if n.id == select_id), None) if select_id else None
|
|
371
549
|
if self.shown:
|
|
@@ -441,12 +619,14 @@ class BearUI(App):
|
|
|
441
619
|
editor = self.query_one("#editor", SecretTextArea)
|
|
442
620
|
editor.secret_values = list(secrets or [])
|
|
443
621
|
editor.text = text
|
|
622
|
+
self._editor_original = text
|
|
444
623
|
editor.border_title = title
|
|
445
624
|
self.query_one("#preview-pane").styles.display = "none"
|
|
446
625
|
editor.styles.display = "block"
|
|
447
626
|
editor.focus()
|
|
448
627
|
if cursor:
|
|
449
628
|
editor.cursor_location = cursor
|
|
629
|
+
self.query_one(KeyBar).show(EDIT_KEYS_BEAR, EDIT_KEYS_TUI)
|
|
450
630
|
self.refresh_bindings()
|
|
451
631
|
|
|
452
632
|
def _exit_editor(self) -> None:
|
|
@@ -457,6 +637,7 @@ class BearUI(App):
|
|
|
457
637
|
self.query_one("#results", OptionList).focus()
|
|
458
638
|
if note := self._selected():
|
|
459
639
|
self.query_one("#preview-content", Static).update(self._preview(note))
|
|
640
|
+
self.query_one(KeyBar).show(BROWSE_KEYS_BEAR, BROWSE_KEYS_TUI)
|
|
460
641
|
self.refresh_bindings()
|
|
461
642
|
|
|
462
643
|
def action_edit_selected(self) -> None:
|
|
@@ -490,7 +671,17 @@ class BearUI(App):
|
|
|
490
671
|
|
|
491
672
|
def action_back_or_quit(self) -> None:
|
|
492
673
|
if self.edit_mode:
|
|
493
|
-
self.
|
|
674
|
+
if self.query_one("#editor", SecretTextArea).text != self._editor_original:
|
|
675
|
+
|
|
676
|
+
def on_answer(answer: str | None) -> None:
|
|
677
|
+
if answer == "discard":
|
|
678
|
+
self._exit_editor()
|
|
679
|
+
elif answer == "save":
|
|
680
|
+
self.action_save_edit()
|
|
681
|
+
|
|
682
|
+
self.push_screen(ConfirmDiscard(), on_answer)
|
|
683
|
+
else:
|
|
684
|
+
self._exit_editor()
|
|
494
685
|
elif isinstance(self.focused, Input):
|
|
495
686
|
self.query_one("#results", OptionList).focus()
|
|
496
687
|
else:
|
|
@@ -520,9 +711,29 @@ class BearUI(App):
|
|
|
520
711
|
|
|
521
712
|
self.push_screen(TagScreen(f"Remove tag from “{note.title}”", note.tags), on_done)
|
|
522
713
|
|
|
714
|
+
def action_switch_view(self, view: str) -> None:
|
|
715
|
+
if view == self.view:
|
|
716
|
+
return
|
|
717
|
+
self.view = view
|
|
718
|
+
self._select_id = None
|
|
719
|
+
self.notify({"notes": "Notes", "archive": "Archive", "trash": "Trash"}[view] + " view")
|
|
720
|
+
self._set_list_loading(True)
|
|
721
|
+
self._rehydrate()
|
|
722
|
+
|
|
723
|
+
def action_help(self) -> None:
|
|
724
|
+
self.push_screen(HelpScreen())
|
|
725
|
+
|
|
726
|
+
def action_cursor_down(self) -> None:
|
|
727
|
+
if len(self.screen_stack) == 1:
|
|
728
|
+
self.query_one("#results", OptionList).action_cursor_down()
|
|
729
|
+
|
|
730
|
+
def action_cursor_up(self) -> None:
|
|
731
|
+
if len(self.screen_stack) == 1:
|
|
732
|
+
self.query_one("#results", OptionList).action_cursor_up()
|
|
733
|
+
|
|
523
734
|
def action_refresh(self) -> None:
|
|
735
|
+
self._set_list_loading(True)
|
|
524
736
|
self._rehydrate()
|
|
525
|
-
self.notify("Reloading from Bear…")
|
|
526
737
|
|
|
527
738
|
def action_archive_note(self) -> None:
|
|
528
739
|
self._file_away("archive")
|
|
@@ -575,7 +786,17 @@ class BearUI(App):
|
|
|
575
786
|
message = f"Tagged with {tag!r}" if add else f"Removed tag {tag!r}"
|
|
576
787
|
self._after_write(note.id, fresh, message, "Tag" if add else "Untag", edit_after=True)
|
|
577
788
|
|
|
789
|
+
def _in_current_view(self, note: Note) -> bool:
|
|
790
|
+
if self.view == "trash":
|
|
791
|
+
return note.trashed
|
|
792
|
+
if self.view == "archive":
|
|
793
|
+
return note.archived and not note.trashed
|
|
794
|
+
return not note.trashed and not note.archived
|
|
795
|
+
|
|
578
796
|
def _file_away(self, operation: str) -> None:
|
|
797
|
+
if (operation == "trash" and self.view == "trash") or (operation == "archive" and self.view == "archive"):
|
|
798
|
+
self.notify(f"Already in the {self.view} view", severity="warning")
|
|
799
|
+
return
|
|
579
800
|
if note := self._selected():
|
|
580
801
|
self._file_away_worker(note, operation)
|
|
581
802
|
|
|
@@ -615,9 +836,8 @@ class BearUI(App):
|
|
|
615
836
|
|
|
616
837
|
|
|
617
838
|
def run_ui(
|
|
618
|
-
notes: list[Note],
|
|
619
839
|
fuzzy: bool = False,
|
|
620
840
|
db_path: Path = DEFAULT_DB_PATH,
|
|
621
841
|
tag_filter: str | None = None,
|
|
622
842
|
) -> None:
|
|
623
|
-
BearUI(
|
|
843
|
+
BearUI(fuzzy=fuzzy, db_path=db_path, tag_filter=tag_filter).run()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|