1
|
|
|
# SPDX-License-Identifier: LGPL-3.0-only |
2
|
|
|
|
3
|
1 |
|
"""Functions to edit documents and items.""" |
4
|
1 |
|
|
5
|
1 |
|
import os |
6
|
1 |
|
import subprocess |
7
|
1 |
|
import sys |
8
|
|
|
import tempfile |
9
|
1 |
|
import time |
10
|
1 |
|
from distutils.spawn import find_executable |
11
|
|
|
|
12
|
1 |
|
from doorstop import common |
13
|
|
|
from doorstop.common import DoorstopError |
14
|
1 |
|
|
15
|
|
|
LAUNCH_DELAY = 0.5 # number of seconds to let a program try to launch |
16
|
|
|
|
17
|
|
|
log = common.logger(__name__) |
18
|
|
|
|
19
|
|
|
|
20
|
|
|
def edit(path, tool=None): |
21
|
|
|
"""Open a file and wait for the default editor to exit. |
22
|
|
|
|
23
|
|
|
:param path: path of file to open |
24
|
|
|
:param tool: path of alternate editor |
25
|
|
|
|
26
|
|
|
:return: launched process |
27
|
|
|
|
28
|
|
|
""" |
29
|
|
|
process = launch(path, tool=tool) |
30
|
|
|
if process: |
31
|
|
|
try: |
32
|
|
|
process.wait() |
33
|
|
|
except KeyboardInterrupt: |
34
|
|
|
log.debug("user cancelled") |
35
|
|
|
finally: |
36
|
|
|
if process.returncode is None: |
37
|
|
|
process.terminate() |
38
|
|
|
log.warning("force closed editor") |
39
|
|
|
log.debug("process exited: {}".format(process.returncode)) |
40
|
|
|
|
41
|
|
|
|
42
|
|
|
def edit_tmp_content(title=None, original_content=None, tool=None): |
43
|
|
|
"""Edit content in a temporary file and return the saved content. |
44
|
|
|
|
45
|
|
|
:param title: text that will appear in the name of the temporary file. |
46
|
|
|
If not given, name is only random characters. |
47
|
|
|
:param original_content: content to insert in the temporary file before |
48
|
|
|
opening it with the editor. If not given, file is empty. |
49
|
|
|
Must be a string object. |
50
|
|
|
:param tool: path of alternate editor |
51
|
|
|
|
52
|
|
|
:return: content of the temporary file after user closes the editor. |
53
|
|
|
|
54
|
|
|
""" |
55
|
|
|
# Create a temporary file to edit the text |
56
|
|
|
tmp_fd, tmp_path = tempfile.mkstemp(prefix='{}_'.format(title), text=True) |
57
|
|
|
os.close(tmp_fd) # release the file descriptor because it is not needed |
58
|
|
|
with open(tmp_path, 'w') as tmp_f: |
59
|
|
|
tmp_f.write(original_content) |
60
|
|
|
|
61
|
|
|
# Open the editor to edit the temporary file with the original text |
62
|
|
|
edit(tmp_path, tool=tool) |
63
|
|
|
|
64
|
|
|
# Read the edited text and remove the tmp file |
65
|
|
|
with open(tmp_path, 'r') as tmp_f: |
66
|
|
|
edited_content = tmp_f.read() |
67
|
|
|
os.remove(tmp_path) |
68
|
|
|
|
69
|
|
|
return edited_content |
70
|
|
|
|
71
|
|
|
|
72
|
|
|
def launch(path, tool=None): |
73
|
|
|
"""Open a file using the default editor. |
74
|
|
|
|
75
|
|
|
:param path: path of file to open |
76
|
|
|
:param tool: path of alternate editor |
77
|
|
|
|
78
|
|
|
:raises: :class:`~doorstop.common.DoorstopError` no default editor |
79
|
|
|
or editor unavailable |
80
|
|
|
|
81
|
|
|
:return: launched process if long-running, else None |
82
|
|
|
|
83
|
|
|
""" |
84
|
|
|
# Determine how to launch the editor |
85
|
|
|
if tool: |
86
|
|
|
args = [tool, path] |
87
|
|
|
elif sys.platform.startswith('darwin'): |
88
|
|
|
args = ['open', path] |
89
|
|
|
elif os.name == 'nt': |
90
|
|
|
cygstart = find_executable('cygstart') |
91
|
|
|
if cygstart: |
92
|
|
|
args = [cygstart, path] |
93
|
|
|
else: |
94
|
|
|
args = ['start', path] |
95
|
|
|
elif os.name == 'posix': |
96
|
|
|
args = ['xdg-open', path] |
97
|
|
|
|
98
|
|
|
# Launch the editor |
99
|
|
|
try: |
100
|
|
|
log.info("opening '{}'...".format(path)) |
101
|
|
|
process = _call(args) |
|
|
|
|
102
|
|
|
except FileNotFoundError: |
103
|
|
|
raise DoorstopError("editor not found: {}".format(args[0])) |
104
|
|
|
|
105
|
|
|
# Wait for the editor to launch |
106
|
|
|
time.sleep(LAUNCH_DELAY) |
107
|
|
|
if process.poll() is None: |
108
|
|
|
log.debug("process is running...") |
109
|
|
|
else: |
110
|
|
|
log.debug("process exited: {}".format(process.returncode)) |
111
|
|
|
if process.returncode != 0: |
112
|
|
|
raise DoorstopError("no default editor for: {}".format(path)) |
113
|
|
|
|
114
|
|
|
# Return the process if it's still running |
115
|
|
|
return process if process.returncode is None else None |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
def _call(args): |
119
|
|
|
"""Call a program with arguments and return the process.""" |
120
|
|
|
log.debug("$ {}".format(' '.join(args))) |
121
|
|
|
process = subprocess.Popen(args) |
122
|
|
|
return process |
123
|
|
|
|