-
Notifications
You must be signed in to change notification settings - Fork 21
/
main.py
283 lines (220 loc) · 7.93 KB
/
main.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
#!/usr/bin/env python3
import os
import re
import logging
import subprocess
import warnings
from pathlib import Path
from shutil import copy
from daemonize import Daemonize
import click
import platform
from .picker import pick
from appdirs import user_config_dir
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
log = logging.getLogger("inkscape-figures")
def inkscape(path):
with warnings.catch_warnings():
# leaving a subprocess running after interpreter exit raises a
# warning in Python3.7+
warnings.simplefilter("ignore", ResourceWarning)
subprocess.Popen(["inkscape", str(path)])
def indent(text, indentation=0):
lines = text.split("\n")
return "\n".join(" " * indentation + line for line in lines)
def beautify(name):
return name.replace("_", " ").replace("-", " ").title()
def import_file(name, path):
import importlib.util as util
spec = util.spec_from_file_location(name, path)
module = util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
# Load user config
user_dir = Path(user_config_dir("inkscape-figures", "Castel"))
if not user_dir.is_dir():
user_dir.mkdir()
roots_file = user_dir / "roots"
template = user_dir / "template.svg"
config = user_dir / "config.py"
if not roots_file.is_file():
roots_file.touch()
if not template.is_file():
source = str(Path(__file__).parent / "template.svg")
destination = str(template)
copy(source, destination)
def add_root(path):
path = str(path)
roots = get_roots()
if path in roots:
return None
roots.append(path)
roots_file.write_text("\n".join(roots))
def get_roots():
return [root for root in roots_file.read_text().split("\n") if root != ""]
@click.group()
def cli():
pass
@cli.command()
@click.option("--daemon/--no-daemon", default=True)
def watch(daemon):
"""
Watches for figures.
"""
if platform.system() == "Linux":
watcher_cmd = watch_daemon_inotify
else:
watcher_cmd = watch_daemon_fswatch
if daemon:
daemon = Daemonize(app="inkscape-figures", pid="/tmp/inkscape-figures.pid", action=watcher_cmd)
daemon.start()
log.info("Watching figures.")
else:
log.info("Watching figures.")
watcher_cmd()
def maybe_recompile_figure(filepath):
filepath = Path(filepath)
# A file has changed
if filepath.suffix != ".svg":
log.debug("File has changed, but is nog an svg {}".format(filepath.suffix))
return
log.info("Recompiling %s", filepath)
pdf_path = filepath.parent / (filepath.stem + ".pdf")
name = filepath.stem
inkscape_version = subprocess.check_output(["inkscape", "--version"], universal_newlines=True)
log.debug(inkscape_version)
# Convert
# - 'Inkscape 0.92.4 (unknown)' to [0, 92, 4]
# - 'Inkscape 1.1-dev (3a9df5bcce, 2020-03-18)' to [1, 1]
# - 'Inkscape 1.0rc1' to [1, 0]
inkscape_version = re.findall(r"[0-9.]+", inkscape_version)[0]
inkscape_version_number = [int(part) for part in inkscape_version.split(".")]
# Right-pad the array with zeros (so [1, 1] becomes [1, 1, 0])
inkscape_version_number = inkscape_version_number + [0] * (3 - len(inkscape_version_number))
# Tuple comparison is like version comparison
if inkscape_version_number < [1, 0, 0]:
command = [
"inkscape",
"--export-area-page",
"--export-dpi",
"300",
"--export-pdf",
pdf_path,
"--export-latex",
filepath,
]
else:
command = [
"inkscape",
filepath,
"--export-area-page",
"--export-dpi",
"300",
"--export-type=pdf",
"--export-latex",
"--export-filename",
pdf_path,
]
log.debug("Running command:")
log.debug(" ".join(str(e) for e in command))
# Recompile the svg file
completed_process = subprocess.run(command)
if completed_process.returncode != 0:
log.error("Return code %s", completed_process.returncode)
else:
log.debug("Command succeeded")
def watch_daemon_inotify():
import inotify.adapters
from inotify.constants import IN_CLOSE_WRITE
while True:
roots = get_roots()
# Watch the file with contains the paths to watch
# When this file changes, we update the watches.
i = inotify.adapters.Inotify()
i.add_watch(str(roots_file), mask=IN_CLOSE_WRITE)
# Watch the actual figure directories
log.info("Watching directories: " + ", ".join(get_roots()))
for root in roots:
try:
i.add_watch(root, mask=IN_CLOSE_WRITE)
except Exception:
log.debug("Could not add root %s", root)
for event in i.event_gen(yield_nones=False):
(_, type_names, path, filename) = event
# If the file containing figure roots has changes, update the
# watches
if path == str(roots_file):
log.info("The roots file has been updated. Updating watches.")
for root in roots:
try:
i.remove_watch(root)
log.debug("Removed root %s", root)
except Exception:
log.debug("Could not remove root %s", root)
# Break out of the loop, setting up new watches.
break
# A file has changed
path = Path(path) / filename
maybe_recompile_figure(path)
def watch_daemon_fswatch():
while True:
roots = get_roots()
log.info("Watching directories: " + ", ".join(roots))
# Watch the figures directories, as weel as the config directory
# containing the roots file (file containing the figures to the figure
# directories to watch). If the latter changes, restart the watches.
with warnings.catch_warnings():
warnings.simplefilter("ignore", ResourceWarning)
p = subprocess.Popen(["fswatch", *roots, str(user_dir)], stdout=subprocess.PIPE, universal_newlines=True)
while True:
filepath = p.stdout.readline().strip()
# If the file containing figure roots has changes, update the
# watches
if filepath == str(roots_file):
log.info("The roots file has been updated. Updating watches.")
p.terminate()
log.debug("Removed main watch %s")
break
maybe_recompile_figure(filepath)
@cli.command()
@click.argument("title")
@click.argument("root", default=os.getcwd(), type=click.Path(exists=False, file_okay=False, dir_okay=True))
def create(title, root):
"""
Creates a figure.
First argument is the title of the figure
Second argument is the figure directory.
"""
title = title.strip()
file_name = title.replace(" ", "-").lower() + ".svg"
figures = Path(root).absolute()
if not figures.exists():
figures.mkdir()
figure_path = figures / file_name
# If a file with this name already exists, append a '2'.
if figure_path.exists():
print(title + " 2")
return
copy(str(template), str(figure_path))
add_root(figures)
inkscape(figure_path)
@cli.command()
@click.argument("root", default=os.getcwd(), type=click.Path(exists=True, file_okay=False, dir_okay=True))
def edit(root):
"""
Edits a figure.
First argument is the figure directory.
"""
figures = Path(root).absolute()
# Find svg files and sort them
files = figures.glob("*.svg")
files = sorted(files, key=lambda f: f.stat().st_mtime, reverse=True)
# Open a selection dialog using a gui picker (choose)
names = [beautify(f.stem) for f in files]
_, index, selected = pick(names)
if selected:
path = files[index]
add_root(figures)
inkscape(path)
if __name__ == "__main__":
cli()