-
Notifications
You must be signed in to change notification settings - Fork 0
/
settings.py
433 lines (383 loc) · 14.8 KB
/
settings.py
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
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
"""Manages the app's settings.
Settings
--------
'body background color' : str
The color of the background of the site as a hex RGB string.
'body hover color' : str
The color of links in the body when they are hovered over, as a hex RGB
string.
'body link color' : str
The color of links in the body, as a hex RGB string.
'copyright text' : str
The copyright notice that will appear at the bottom of the index page.
'header background color' : str
The color of the background of the header as a hex RGB string.
'header hover color' : str
The color of links in the header when they are hovered over, as a hex
RGB string.
'header text color' : str
The color of the text in the header as a hex RGB string.
'hide chrono index dates' : bool
If true, file creation dates will not be shown in the chronological
index.
'hide tags' : bool
If true, tags will be removed from the copied zettels when generating
the site.
'internal html link prefix' : str
Text that will be prepended to internal links. This setting can be an empty
string.
'patterns' : Settings
'absolute attachment link' : re.Pattern
The pattern of a markdown link containing an absolute path to a file.
The first capture group is the file's name and extension.
'h1 content' : re.Pattern
The pattern of a header of level 1.
'link path' : re.Pattern
The pattern of a markdown link containing a path to a file or website,
and the path may be either relative or absolute.
'md ext in link' : re.Pattern
The pattern of a markdown file path extension in a markdown link.
'md link' : re.Pattern
The pattern of a markdown link.
'published tag' : re.Pattern
The pattern of the ``#published`` tag.
'single codeblock' : re.Pattern
The pattern of a codeblock delimited by single backticks (an inline
codeblock).
'tag' : re.Pattern
The pattern of a tag, including a #. Assumes the tag is not at the very
beginning of the string.
'triple codeblock' : re.Pattern
The pattern of a codeblock delimited by triple backticks.
'zk id' : re.Pattern
The pattern of a zettel ID.
'root pages' : List[str]
The list of file names (excluding the file extension) of the pages in the
root folder of the site.
'site folder path' : str
The absolute path to the root folder of the site's files.
'site subfolder name' : str
The name of the folder within the site folder that most of the HTML
pages will be placed in by default.
'site title' : str
The title that will appear at the top of the site.
'zettel file types' : List[str]
The list of file extensions that are considered to be zettels, including
the period. All of the letters are lowercase. If this is changed, the
"md ext in link" pattern will need to be updated.
'zettelkasten path' : str
The absolute path to the zettelkasten folder.
'zk link end' : str
The text that indicates the end of an internal zettelkasten link.
'zk link start' : str
The text that indicates the start of an internal zettelkasten link.
"""
import os
import re
import sys
from copy import deepcopy
from datetime import datetime
from tkinter.filedialog import askdirectory
import PySimpleGUI as sg # https://pysimplegui.readthedocs.io/en/latest/
from app_settings_dict import Settings # https://pypi.org/project/app-settings-dict/
def show_settings_window(settings: Settings) -> Settings:
"""Runs the settings menu and returns the settings.
Parameters
----------
settings : Settings
The current application settings.
"""
window = create_settings_window(settings.dump_to_dict())
new_settings_obj = deepcopy(settings)
settings_are_valid = False
while not settings_are_valid:
event, new_settings_dict = window.read()
if event == sg.WIN_CLOSED:
sys.exit(0)
new_settings_dict = nest_items(filter_items(new_settings_dict))
new_settings_obj.load_from_dict(new_settings_dict)
settings_are_valid = validate_settings(new_settings_obj)
window.close()
if event == "cancel":
return settings
settings = new_settings_obj
settings.save()
return settings
def request_site_folder_path() -> str:
"""Prompts the user for the site's root folder path.
Returns
-------
str
The path to the site's root folder.
"""
sg.PopupOK("Please select the folder that will contain the site's files.")
return askdirectory(title="site folder", mustexist=True)
def request_zettelkasten_path() -> str:
"""Prompts the user for the zettelkasten folder path.
Returns
-------
str
The path to the zettelkasten folder.
"""
sg.PopupOK("Please select the folder that contains the zettelkasten.")
return askdirectory(title="zettelkasten folder", mustexist=True)
settings_folder_path = os.path.dirname(os.path.abspath(__file__))
settings_file_path = os.path.join(settings_folder_path, "settings.json")
this_year = datetime.now().year
settings = Settings(
settings_file_path=settings_file_path,
prompt_user_for_all_settings=show_settings_window,
default_factories={
"site folder path": request_site_folder_path,
"zettelkasten path": request_zettelkasten_path,
},
data={
"body background color": "#fffafa", # snow
"body hover color": "#3d550c", # olive green
"body link color": "#59981a", # green
"copyright text": f"© {this_year}, your name",
"header background color": "#81b622", # lime green
"header hover color": "#3d550c", # olive green
"header text color": "#ecf87f", # yellow green
"hide chrono index dates": True,
"hide tags": True,
"internal html link prefix": "[§] ",
"patterns": Settings(
setting_dumper=lambda x: x.pattern,
setting_loader=re.compile,
data={
"absolute attachment link": re.compile(
r"(?<=]\()(?:file://)?(?:[a-zA-Z]:|/)"
r"[^\n]*?([^\\/\n]+\.[a-zA-Z0-9_-]+)(?=\))"
),
"link path": re.compile(r"(?<=]\().+(?=\))"),
"h1 content": re.compile(r"^# (.+)$"),
"md ext in link": re.compile(r"(?i)(?<=\S)\.m(d|arkdown)(?=\))"),
"md link": re.compile(r"\[(.+)]\((.+)\)"),
"published tag": re.compile(r"(?<=\s)#published(?=\s)"),
"single codeblock": re.compile(r"(`[^`]+?`)"),
"tag": re.compile(r"(?<=\s)#[a-zA-Z0-9_-]+"),
"triple codeblock": re.compile(r"(?<=\n)(`{3}(.|\n)*?(?<=\n)`{3})"),
"zk id": re.compile(r"(\d{14})"),
},
),
"root pages": [
"index",
"about",
"alphabetical-index",
"categorical-index",
],
"site folder path": "",
"site subfolder name": "pages",
"site title": "Site Title Here",
"zettel file types": [".md", ".markdown"],
"zettelkasten path": "",
"zk link end": "]]",
"zk link start": "[[",
},
)
def get_zk_link_contents_pattern() -> re.Pattern:
"""Gets the pattern of the contents of a zettelkasten link.
The pattern must contain either 0 or 1 capture groups.
"""
zk_link_start = re.escape(settings["zk link start"])
zk_link_end = re.escape(settings["zk link end"])
return re.compile(f"{zk_link_start}(.+?){zk_link_end}")
def get_zk_id_not_in_link_pattern() -> re.Pattern:
"""Gets the pattern of a zettel ID that is not in a zettelkasten link."""
zk_link_start = re.escape(settings["zk link start"])
zk_id_pattern = settings["patterns"]["zk id"].pattern
return re.compile(rf"(?<!\\)(?<!{zk_link_start}){zk_id_pattern}")
def create_settings_window(settings: dict) -> sg.Window:
"""Creates and displays the settings menu.
Parameters
----------
settings : dict
The settings data dictionary.
"""
sg.theme("DarkAmber")
general_tab_layout = [
create_text_chooser("site title: ", "site title", settings),
create_text_chooser("copyright text: ", "copyright text", settings),
create_text_chooser("site subfolder name: ", "site subfolder name", settings),
create_text_chooser(
"internal HTML link prefix: ", "internal html link prefix", settings
),
create_text_chooser(
"ID regular expression: ", "patterns.zk id", settings["patterns"]
),
create_text_chooser("link start: ", "zk link start", settings),
create_text_chooser("link end: ", "zk link end", settings),
create_folder_chooser(
"site folder path (folder): ", "site folder path", settings
),
create_folder_chooser(
"zettelkasten path (folder): ", "zettelkasten path", settings
),
create_checkbox("hide tags", "hide tags", settings),
create_checkbox(
"hide dates in the chronological index", "hide chrono index dates", settings
),
]
color_tab_layout = [
create_color_chooser(
"body background color: ", "body background color", settings
),
create_color_chooser(
"header background color: ", "header background color", settings
),
create_color_chooser("header text color: ", "header text color", settings),
create_color_chooser("header hover color: ", "header hover color", settings),
create_color_chooser("body link color: ", "body link color", settings),
create_color_chooser("body hover color: ", "body hover color", settings),
]
layout = [
[
sg.TabGroup(
[
[
sg.Tab("general", general_tab_layout),
sg.Tab("color", color_tab_layout),
]
]
)
],
[sg.HorizontalSeparator()],
[sg.Button("save"), sg.Button("cancel")],
]
return sg.Window("Aurora settings", layout)
def create_text_chooser(title: str, key: str, settings: dict) -> list:
"""Creates PySimpleGUI elements for choosing text.
Parameters
----------
title : str
The text that appears next to the input element.
key : str
The key of the setting. If the key contains periods, only the last part
after all the periods is used as the key when accessing the settings
dictionary.
settings : dict
The settings data dictionary.
"""
try:
default_text = settings[key.split(".")[-1]]
except KeyError:
default_text = settings[key.split(".")[-1]] = ""
finally:
return [sg.Text(title), sg.Input(key=key, default_text=default_text)]
def create_checkbox(title: str, key: str, settings: dict) -> list:
"""Creates PySimpleGUI elements for a labelled checkbox.
Parameters
----------
title : str
The text that appears next to the checkbox.
key : str
The key of the setting.
settings : dict
The settings data dictionary.
"""
return [sg.Checkbox(title, key=key, default=settings[key])]
def create_folder_chooser(title: str, key: str, settings: dict) -> list:
"""Creates PySimpleGUI elements for choosing a folder.
Parameters
----------
title : str
The text that appears next to the input element.
key : str
The key of the setting.
settings : dict
The settings data dictionary.
"""
return [
sg.Text(title),
sg.FolderBrowse(target=key),
sg.Input(key=key, default_text=settings[key]),
]
def create_color_chooser(title: str, key: str, settings: dict) -> list:
"""Creates PySimpleGUI elements for choosing a color.
Parameters
----------
title : str
The text that appears next to the input element.
key : str
The key of the setting.
settings : dict
The settings data dictionary.
"""
return [
sg.Text(title),
sg.ColorChooserButton("choose", target=key),
sg.Input(key=key, default_text=settings[key]),
]
def filter_items(settings: dict) -> dict:
"""Removes some dict items automatically generated by PySimpleGUI.
Removes all items with keys that are not strings or that start with
an uppercase letter.
Parameters
----------
settings : dict
The settings to filter.
"""
new_settings = dict()
for key, value in settings.items():
if isinstance(key, str):
if key[0].islower():
new_settings[key] = value
return new_settings
def nest_items(settings: dict) -> dict:
"""Nests dict items whose keys contain one or more periods.
For example, if a setting is `settings["a.b.c"] = "value"`, it is moved to
`settings["a"]["b"]["c"] = "value"`. Any settings whose keys do not contain
periods are not changed.
Parameters
----------
settings : dict
The settings to try to nest.
"""
new_settings = dict()
for key, value in settings.items():
if "." in key:
new_settings[key.split(".")[0]] = nest_items({key.split(".")[1]: value})
else:
new_settings[key] = value
return new_settings
def validate_settings(settings: Settings) -> bool:
"""Detects any invalid settings and can show a popup with an error message.
Parameters
----------
settings : Settings
The settings to validate.
"""
if "patterns" in settings and settings["patterns"]["zk id"].groups > 1:
sg.popup("The ID regular expression must have one or no capturing groups.")
return False
if not os.path.exists(settings["zettelkasten path"]) or not os.path.isdir(
settings["zettelkasten path"]
):
sg.popup("The zettelkasten path does not exist.")
return False
if not os.path.exists(settings["site folder path"]) or not os.path.isdir(
settings["site folder path"]
):
sg.popup("The site folder path does not exist.")
return False
this_dir, _ = os.path.split(__file__)
settings["site folder path"] = os.path.normpath(settings["site folder path"])
settings["zettelkasten path"] = os.path.normpath(settings["zettelkasten path"])
if 3 > len({this_dir, settings["site folder path"], settings["zettelkasten path"]}):
error_message = (
"Error: the zettelkasten, the website's files, and this program's files"
" should be in different folders."
)
sg.popup(error_message)
return False
for key, value in settings.items():
if isinstance(value, str):
if not value and key != "internal html link prefix":
sg.popup(
'Each setting must be given a value, except the "internal html link'
' prefix" setting.'
)
return False
return True
settings.load(fallback_option="prompt user")