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) |
||
0 ignored issues
–
show
introduced
by
Loading history...
|
|||
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 |