tklr-dgraham 0.0.0rc11__py3-none-any.whl

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 tklr-dgraham might be problematic. Click here for more details.

tklr/__init__.py ADDED
File without changes
tklr/cli/main.py ADDED
@@ -0,0 +1,253 @@
1
+ import sys
2
+ import os
3
+ import click
4
+ from pathlib import Path
5
+ from rich import print
6
+
7
+ from tklr.item import Item
8
+ from tklr.controller import Controller
9
+ from tklr.model import DatabaseManager, UrgencyComputer
10
+ from tklr.view import DynamicViewApp
11
+ from tklr.tklr_env import TklrEnvironment
12
+ from tklr.view_agenda import run_agenda_view
13
+ from tklr.versioning import get_version
14
+
15
+ VERSION = get_version()
16
+ print(f"{VERSION = }")
17
+
18
+
19
+ def ensure_database(db_path: str, env: TklrEnvironment):
20
+ if not Path(db_path).exists():
21
+ print(
22
+ f"[yellow]⚠️ [/yellow]Database not found. Creating new database at {db_path}"
23
+ )
24
+ dbm = DatabaseManager(db_path, env)
25
+ dbm.setup_database()
26
+
27
+
28
+ def format_tokens(tokens, width=80):
29
+ return " ".join([f"{t['token'].strip()}" for t in tokens])
30
+
31
+
32
+ def get_raw_from_file(path: str) -> str:
33
+ with open(path, "r", encoding="utf-8") as f:
34
+ return f.read().strip()
35
+
36
+
37
+ def get_raw_from_editor() -> str:
38
+ result = edit_entry()
39
+ return result or ""
40
+
41
+
42
+ def get_raw_from_stdin() -> str:
43
+ return sys.stdin.read().strip()
44
+
45
+
46
+ @click.group()
47
+ @click.version_option(VERSION, prog_name="tklr", message="%(prog)s version %(version)s")
48
+ @click.option(
49
+ "--home",
50
+ help="Override the Tklr workspace directory (equivalent to setting $TKLR_HOME).",
51
+ )
52
+ @click.option("--verbose", "-v", is_flag=True, help="Enable verbose output")
53
+ @click.pass_context
54
+ def cli(ctx, home, verbose):
55
+ """Tklr CLI – manage your reminders from the command line."""
56
+ if home:
57
+ os.environ["TKLR_HOME"] = (
58
+ home # Must be set before TklrEnvironment is instantiated
59
+ )
60
+
61
+ env = TklrEnvironment()
62
+ env.ensure(init_db_fn=lambda path: ensure_database(path, env))
63
+ config = env.load_config()
64
+
65
+ ctx.ensure_object(dict)
66
+ ctx.obj["ENV"] = env
67
+ ctx.obj["DB"] = env.db_path
68
+ ctx.obj["CONFIG"] = config
69
+ ctx.obj["VERBOSE"] = verbose
70
+
71
+
72
+ @cli.command()
73
+ @click.argument("entry", nargs=-1)
74
+ @click.option(
75
+ "--file",
76
+ "-f",
77
+ type=click.Path(exists=True),
78
+ help="Path to file with multiple entries.",
79
+ )
80
+ @click.option(
81
+ "--batch",
82
+ is_flag=True,
83
+ help="Use editor to create multiple entries separated by blank lines.",
84
+ )
85
+ @click.pass_context
86
+ def add(ctx, entry, file, batch):
87
+ env = ctx.obj["ENV"]
88
+ db = ctx.obj["DB"]
89
+ verbose = ctx.obj["VERBOSE"]
90
+ bad_items = []
91
+ dbm = DatabaseManager(db, env)
92
+
93
+ def clean_and_split(content: str) -> list[str]:
94
+ """
95
+ Remove comment-like lines (starting with any '#', regardless of spacing)
96
+ and split into entries separated by '...' lines.
97
+ """
98
+ lines = []
99
+ for line in content.splitlines():
100
+ stripped = line.lstrip() # remove leading whitespace
101
+ if not stripped.startswith("#"):
102
+ lines.append(line)
103
+ cleaned = "\n".join(lines)
104
+ return split_entries(cleaned)
105
+
106
+ def split_entries(content: str) -> list[str]:
107
+ """Split raw text into entries using '...' line as separator."""
108
+ return [entry.strip() for entry in content.split("\n...\n") if entry.strip()]
109
+
110
+ def get_entries_from_editor() -> list[str]:
111
+ result = edit_entry()
112
+ if not result:
113
+ return []
114
+ return split_entries(result)
115
+
116
+ def get_entries_from_file(path: str) -> list[str]:
117
+ with open(path, "r", encoding="utf-8") as f:
118
+ content = f.read().strip()
119
+ return split_entries(content)
120
+
121
+ def get_entries_from_stdin() -> list[str]:
122
+ data = sys.stdin.read().strip()
123
+ return split_entries(data)
124
+
125
+ def process_entry(entry_str: str) -> bool:
126
+ exception = False
127
+ msg = None
128
+ try:
129
+ item = Item(raw=entry_str, final=True)
130
+ if not item.parse_ok or not item.itemtype:
131
+ # pm = "\n".join(item.parse_message)
132
+ # tks = "\n".join(item.relative_tokens)
133
+ msg = f"\n[red]✘ Invalid entry[/red] \nentry: {entry_str}\nparse_message: {item.parse_message}\ntokens: {item.relative_tokens}"
134
+ except Exception as e:
135
+ msg = f"\n[red]✘ Internal error during parsing:[/red]\nentry: {entry_str}\nexception: {e}"
136
+
137
+ if msg:
138
+ if verbose:
139
+ print(f"{msg}")
140
+ else:
141
+ bad_items.append(msg)
142
+ return False
143
+
144
+ dry_run = False
145
+ if dry_run:
146
+ print(f"[green]would have added:\n {item = }")
147
+ else:
148
+ dbm.add_item(item)
149
+ # print(
150
+ # f"[green]✔ Added:[/green] {item.subject if hasattr(item, 'subject') else entry_str}"
151
+ # )
152
+ return True
153
+
154
+ # Determine the source of entries
155
+ if file:
156
+ entries = clean_and_split(get_raw_from_file(file))
157
+ elif batch:
158
+ entries = clean_and_split(get_raw_from_editor())
159
+ elif entry:
160
+ entries = clean_and_split(" ".join(entry).strip())
161
+ elif not sys.stdin.isatty():
162
+ entries = clean_and_split(get_raw_from_stdin())
163
+ else:
164
+ print("[bold yellow]No entry provided.[/bold yellow]")
165
+ if click.confirm("Create one or more entries in your editor?", default=True):
166
+ entries = clean_and_split(get_entries_from_editor())
167
+ else:
168
+ print("[yellow]✘ Cancelled.[/yellow]")
169
+ sys.exit(1)
170
+
171
+ if not entries:
172
+ print("[red]✘ No valid entries to add.[/red]")
173
+ sys.exit(1)
174
+
175
+ print(
176
+ f"[blue]➤ Adding {len(entries)} entr{'y' if len(entries) == 1 else 'ies'}[/blue]"
177
+ )
178
+ count = 0
179
+ for e in entries:
180
+ if process_entry(e):
181
+ count += 1
182
+
183
+ dbm.populate_dependent_tables()
184
+ print(
185
+ f"[green]✔ Added {count} entr{'y' if count == 1 else 'ies'} successfully.[/green]"
186
+ )
187
+ if bad_items:
188
+ print("\n\n=== Invalid items ===\n")
189
+ for item in bad_items:
190
+ print(item)
191
+
192
+
193
+ @cli.command()
194
+ @click.pass_context
195
+ def ui(ctx):
196
+ """Launch the Tklr Textual interface."""
197
+ env = ctx.obj["ENV"]
198
+ db = ctx.obj["DB"]
199
+ verbose = ctx.obj["VERBOSE"]
200
+
201
+ if verbose:
202
+ print(f"[blue]Launching UI with database:[/blue] {db}")
203
+
204
+ controller = Controller(db, env)
205
+ DynamicViewApp(controller).run()
206
+
207
+
208
+ @cli.command()
209
+ @click.argument("entry", nargs=-1)
210
+ @click.pass_context
211
+ def check(ctx, entry):
212
+ """Check whether an entry is valid (parsing only)."""
213
+ verbose = ctx.obj["VERBOSE"]
214
+
215
+ if not entry and not sys.stdin.isatty():
216
+ entry = sys.stdin.read().strip()
217
+ else:
218
+ entry = " ".join(entry).strip()
219
+
220
+ if not entry:
221
+ print("[bold red]✘ No entry provided. Use argument or pipe.[/bold red]")
222
+ sys.exit(1)
223
+
224
+ try:
225
+ item = Item(entry)
226
+ if item.parse_ok:
227
+ print("[green]✔ Entry is valid.[/green]")
228
+ if verbose:
229
+ print(f"[blue]Entry:[/blue] {format_tokens(item.relative_tokens)}")
230
+ else:
231
+ print(f"[red]✘ Invalid entry:[/red] {entry!r}")
232
+ print(f" {item.parse_message}")
233
+ if verbose:
234
+ print(f"[blue]Entry:[/blue] {format_tokens(item.relative_tokens)}")
235
+ sys.exit(1)
236
+ except Exception as e:
237
+ print(f"[red]✘ Unexpected error:[/red] {e}")
238
+ sys.exit(1)
239
+
240
+
241
+ @cli.command()
242
+ @click.pass_context
243
+ def agenda(ctx):
244
+ """Launch the Tklr agenda split-screen view."""
245
+ env = ctx.obj["ENV"]
246
+ db = ctx.obj["DB"]
247
+ verbose = ctx.obj["VERBOSE"]
248
+
249
+ if verbose:
250
+ print(f"[blue]Launching agenda view with database:[/blue] {db}")
251
+
252
+ controller = Controller(db, env)
253
+ run_agenda_view(controller)