Issues (16)

doorstop/core/editor.py (1 issue)

Severity
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
The variable args does not seem to be defined for all execution paths.
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