-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.py
More file actions
300 lines (259 loc) · 11.9 KB
/
Copy pathsetup.py
File metadata and controls
300 lines (259 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
#!/usr/bin/env python3
"""One-command first-time setup for PrepDojo.
Runs the scriptable parts of the Quick start, in order:
1. create your personal config.toml from the template
2. generate and install the vault files (dashboard, daily-note template,
QuickAdd config)
3. install + enable the required Obsidian plugins via the Obsidian CLI
A few steps can only be done in Obsidian's GUI (enabling the CLI, toggling
Dataview's JavaScript queries, assigning hotkeys). This script does everything
else and prints exactly what's left at the end.
Usage:
python3 setup.py /path/to/YourVault
Pass your vault's path; it is recorded in config.toml for you.
(PREPDOJO_VAULT env var works too.)
python3 setup.py
No path: offers to install into the current folder, so running it
from inside your vault needs no arguments at all.
Requires Python 3.9+. On 3.9/3.10 it installs the `tomli` package for you;
on 3.11+ nothing extra is needed. Runs on macOS, Linux, and Windows.
"""
from __future__ import annotations
import importlib.util
import os
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Optional
ROOT = Path(__file__).parent
PLUGINS = ["dataview", "quickadd", "calendar"]
def step(n: int, total: int, msg: str) -> None:
print(f"\n[{n}/{total}] {msg}")
def ensure_config() -> bool:
"""Create config.toml from the template if missing. Returns True if created."""
cfg = ROOT / "config.toml"
template = ROOT / "config-template.toml"
if cfg.exists():
print(" config.toml already exists — leaving your settings untouched")
return False
if not template.exists():
sys.exit(" config-template.toml is missing; cannot create config.toml")
cfg.write_bytes(template.read_bytes())
print(" created config.toml from config-template.toml")
return True
def config_vault_path() -> Optional[str]:
"""Read the vault path from config.toml, ignoring the template placeholder."""
cfg = ROOT / "config.toml"
if not cfg.exists():
return None
for line in cfg.read_text(encoding="utf-8").splitlines():
s = line.strip()
if (s.startswith("path ") or s.startswith("path=")) and "=" in s:
val = s.split("=", 1)[1].strip().strip('"').strip("'")
if val and "/absolute/path/to/YourVault" not in val:
return val
return None
def resolve_vault(just_created: bool) -> str:
if len(sys.argv) > 1:
return sys.argv[1]
env = os.environ.get("PREPDOJO_VAULT")
if env:
print(f" using PREPDOJO_VAULT: {env}")
return env
from_config = config_vault_path()
if from_config:
print(f" using vault path from config.toml: {from_config}")
return from_config
# No path given anywhere: offer the folder the command was run from.
cwd = Path.cwd()
if cwd.resolve() == ROOT.resolve():
print("\nNo vault path given, and the current folder is the PrepDojo repo")
print("itself, which can't be a vault. Rerun with your vault's path:")
print(" python3 setup.py /path/to/YourVault")
sys.exit(1)
try:
answer = input(f"\nNo vault path given. Install into the current folder?\n {cwd}\n[y/N]: ")
except EOFError:
print("\nNo interactive terminal and no vault path. Rerun with:")
print(" python3 setup.py /path/to/YourVault")
sys.exit(1)
if answer.strip().lower() in ("y", "yes"):
return str(cwd)
print("Stopped. Rerun with python3 setup.py /path/to/YourVault")
sys.exit(0)
def write_vault_path(vault: str) -> None:
"""Record the vault path in config.toml so every later command — bare
generate.py deploys, prepdojo.py logging — works without flags. Respects a
path the user already set; only fills the commented-out placeholder."""
cfg = ROOT / "config.toml"
text = cfg.read_text(encoding="utf-8")
for line in text.splitlines():
stripped = line.strip()
if (stripped.startswith("path ") or stripped.startswith("path=")) \
and "/absolute/path/to/YourVault" not in stripped:
print(" config.toml already sets a vault path — leaving it as is")
return
# replace an uncommented placeholder line, if present, so it can't linger
text = text.replace('path = "/absolute/path/to/YourVault"\n', "", 1)
resolved = Path(vault).expanduser().resolve()
path_line = f'path = "{resolved}"'
placeholder = '# path = "/absolute/path/to/YourVault"'
if placeholder in text:
text = text.replace(placeholder, path_line, 1)
else:
text = text.replace("[vault]", f"[vault]\n{path_line}", 1)
cfg.write_text(text, encoding="utf-8")
print(f" recorded vault path in config.toml: {resolved}")
def ensure_tomli() -> None:
"""generate.py/prepdojo.py parse config.toml with tomllib, which is only
built in on Python 3.11+. On older Pythons, install the drop-in `tomli`."""
if sys.version_info >= (3, 11):
return # tomllib is in the standard library
if importlib.util.find_spec("tomli") is not None:
return # already available
print(" Python < 3.11 has no built-in TOML parser — installing 'tomli'...")
result = subprocess.run([sys.executable, "-m", "pip", "install", "tomli"])
if result.returncode != 0:
sys.exit(
" Could not install tomli automatically. Install it and rerun:\n"
f" {sys.executable} -m pip install tomli"
)
print(" tomli installed")
def run_generate(vault: str) -> None:
result = subprocess.run(
[sys.executable, str(ROOT / "generate.py"), "--install", vault]
)
if result.returncode != 0:
sys.exit(" generate.py failed — fix the error above, then rerun.")
MANUAL_PLUGINS = (
" Install the three plugins by hand any time:\n"
" Settings → Community plugins → Browse → Dataview, QuickAdd, Calendar."
)
def active_vault(obsidian: str) -> Optional[str]:
"""Path of the vault Obsidian currently has open, or None if undeterminable.
The CLI installs plugins into THIS vault — it can't target an arbitrary
folder — so we use it to make sure we install into the right one."""
result = subprocess.run(
[obsidian, "vault", "info=path"], capture_output=True, text=True
)
if result.returncode != 0:
return None
# Output may carry Obsidian's own warning lines; the path is the last one.
lines = [ln.strip() for ln in result.stdout.splitlines() if ln.strip()]
return lines[-1] if lines else None
def install_plugins(vault: str) -> None:
obsidian = shutil.which("obsidian")
if not obsidian:
print(
" The 'obsidian' command isn't on your PATH, so plugins were NOT\n"
" installed automatically. That's expected if either:\n"
" - you're on Obsidian older than 1.12 (it has no CLI), or\n"
" - you haven't enabled the CLI yet: open Obsidian → Settings →\n"
" General → Command line interface → register it, then rerun this\n"
" script to finish the plugin install automatically.\n"
+ MANUAL_PLUGINS
)
return
# The Obsidian CLI installs into whichever vault is currently open — it
# cannot target a path or an unregistered folder. So refuse to install
# unless the open vault is the one being set up, to avoid dropping plugins
# into the wrong vault.
target = Path(vault).expanduser().resolve()
current = active_vault(obsidian)
if current is None or Path(current).resolve() != target:
print(
" Skipping automatic plugin install. The Obsidian CLI installs into\n"
" the vault currently open in Obsidian, which isn't your target vault:\n"
f" open now: {current or 'unknown'}\n"
f" target: {target}\n"
" Open your target vault in Obsidian, then rerun this script — or:\n"
+ MANUAL_PLUGINS
)
return
# Community plugins can only be installed with Restricted Mode off.
subprocess.run([obsidian, "plugins:restrict", "off"])
failed = []
for pid in PLUGINS:
result = subprocess.run([obsidian, "plugin:install", f"id={pid}", "enable"])
if result.returncode != 0:
failed.append(pid)
installed = [p for p in PLUGINS if p not in failed]
if installed:
print(" installed and enabled: " + ", ".join(installed))
if failed:
print(" could not install: " + ", ".join(failed)
+ " — add them by hand from Settings → Community plugins → Browse")
def ask_preferences(created: bool) -> None:
"""First-run questions; answers are written into config.toml.
Enter skips/accepts the default. Non-interactive runs skip quietly."""
if not created:
return # existing config: don't re-ask, edit config.toml instead
cfg = ROOT / "config.toml"
try:
print(
"\nTwo quick questions (press Enter to skip or accept the default).\n"
"\nPrepDojo can import your recent accepted LeetCode submissions\n"
"automatically — the dashboard's Import button pulls them into your\n"
"log with one click. That needs your LeetCode username (public\n"
"profile data only; no password, nothing is posted)."
)
username = input("LeetCode username (Enter to skip): ").strip().strip('"')
print(
"\nIf you already keep daily notes in this vault, PrepDojo should\n"
"log into the same folder."
)
folder = input('Daily notes folder [Calendar/Daily Notes]: ').strip().strip('"')
except EOFError:
print("\n(no interactive terminal — keeping defaults; edit config.toml to change)")
return
text = cfg.read_text(encoding="utf-8")
if username:
anchor = '# username = "your-leetcode-username"'
if anchor in text:
text = text.replace(anchor, f'username = "{username}"', 1)
if folder and folder != "Calendar/Daily Notes":
anchor = 'daily_notes_folder = "Calendar/Daily Notes"'
if anchor in text:
text = text.replace(anchor, f'daily_notes_folder = "{folder}"', 1)
cfg.write_text(text, encoding="utf-8")
if username or (folder and folder != "Calendar/Daily Notes"):
print(" recorded in config.toml")
def main() -> None:
if sys.version_info < (3, 9):
sys.exit(
f"PrepDojo needs Python 3.9+ (you have {sys.version.split()[0]}).\n"
"Install a newer Python and run it explicitly, e.g. python3.11 setup.py ..."
)
print("PrepDojo setup")
print("==============")
print(
"Before you run this: open your target vault in Obsidian at least once\n"
"(so its CLI can install plugins into it), then quit Obsidian so the\n"
"generated files install cleanly. Step 3 reopens Obsidian for the plugins.\n"
"If the vault open in Obsidian isn't your target, step 3 safely skips and\n"
"tells you how to finish."
)
step(1, 3, "Creating your config")
created = ensure_config()
vault = resolve_vault(created)
if not Path(vault).expanduser().is_dir():
sys.exit(f"Not a directory: {vault}\n"
"(check the path in config.toml's [vault] section)")
write_vault_path(vault)
ask_preferences(created)
step(2, 3, "Generating and installing vault files")
ensure_tomli()
run_generate(vault)
step(3, 3, "Installing Obsidian plugins")
install_plugins(vault)
print(
"\nAlmost there. Finish these in Obsidian (one-time, GUI only):\n"
" 1. Restart Obsidian.\n"
" 2. Dataview settings → enable 'JavaScript queries'.\n"
" 3. Settings → Hotkeys → search 'QuickAdd' → assign hotkeys\n"
" (suggested: Cmd/Ctrl+Shift+L for LeetCode).\n"
"Then open your dashboard note in Reading view and log your first entry."
)
if __name__ == "__main__":
main()