launch-editor-py 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Adam Johnson
4
+ Copyright (c) 2017-present, Yuxi (Evan) You
5
+ Copyright (c) 2015-present, Facebook, Inc.
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
24
+
25
+ ---
26
+
27
+ This package is a Python port of the launch-editor npm package:
28
+ https://github.com/vitejs/launch-editor
29
+
30
+ launch-editor is Copyright (c) 2017-present, Yuxi (Evan) You, and released
31
+ under the MIT License. Its main module is derived from code in
32
+ create-react-app, Copyright (c) 2015-present, Facebook, Inc., also released
33
+ under the MIT License:
34
+ https://github.com/facebook/create-react-app
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: launch-editor-py
3
+ Version: 1.0.0
4
+ Summary: Open files in the user’s editor, optionally at a given line and column.
5
+ Keywords: editor,launch-editor,open-in-editor
6
+ Author: Adam Johnson
7
+ Author-email: Adam Johnson <me@adamj.eu>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Natural Language :: English
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3.15
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Project-URL: Changelog, https://github.com/adamchainz/launch-editor-py/blob/main/CHANGELOG.rst
23
+ Project-URL: Funding, https://adamj.eu/books/
24
+ Project-URL: Repository, https://github.com/adamchainz/launch-editor-py
25
+ Description-Content-Type: text/x-rst
26
+
27
+ ================
28
+ launch-editor-py
29
+ ================
30
+
31
+ .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/launch-editor-py/main.yml.svg?branch=main&style=for-the-badge
32
+ :target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
33
+
34
+ .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge
35
+ :target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
36
+
37
+ .. image:: https://img.shields.io/pypi/v/launch-editor-py.svg?style=for-the-badge
38
+ :target: https://pypi.org/project/launch-editor-py/
39
+
40
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge
41
+ :target: https://github.com/psf/black
42
+
43
+ .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge
44
+ :target: https://github.com/pre-commit/pre-commit
45
+ :alt: pre-commit
46
+
47
+ ----
48
+
49
+ Open files in the user’s editor, optionally at a given line and column.
50
+
51
+ A Python port of the `launch-editor <https://github.com/vitejs/launch-editor>`__ npm package, as used by Vite to open files from the browser.
52
+ It detects which editor is running, and knows how to pass a line and column to many editors.
53
+
54
+ ----
55
+
56
+ **Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
57
+
58
+ ----
59
+
60
+ Requirements
61
+ ------------
62
+
63
+ Python 3.11 to 3.15 supported.
64
+
65
+ Installation
66
+ ------------
67
+
68
+ 1. Install with **pip**:
69
+
70
+ .. code-block:: sh
71
+
72
+ python -m pip install launch-editor-py
73
+
74
+ The package imports as ``launch_editor``.
75
+
76
+ Usage
77
+ -----
78
+
79
+ Call ``launch_editor()`` with a filename, and optionally a line and column:
80
+
81
+ .. code-block:: python
82
+
83
+ from launch_editor import LaunchEditorError, launch_editor
84
+
85
+ try:
86
+ launch_editor("src/example.py", line=12, column=4)
87
+ except LaunchEditorError as exc:
88
+ print(f"Could not open editor: {exc}")
89
+
90
+ API
91
+ ---
92
+
93
+ ``launch_editor(filename, line=None, column=None, *, editor=None)``
94
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
95
+
96
+ Open ``filename`` in the user’s editor, at ``line`` and ``column`` if given, and return the editor’s ``subprocess.Popen`` object.
97
+
98
+ The editor is picked by ``guess_editor()``, passing it ``editor``.
99
+
100
+ GUI editors start in the background, and the function returns immediately.
101
+ If a GUI editor exits with an error, a warning is logged to the ``launch_editor`` logger.
102
+
103
+ Terminal editors, like Vim, take over the current terminal until they exit.
104
+ While one is open, further attempts to open a terminal editor raise ``LaunchEditorError``.
105
+
106
+ Raises ``LaunchEditorError`` if the file does not exist, no editor is found, or the editor cannot be run.
107
+
108
+ ``guess_editor(editor=None)``
109
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
110
+
111
+ Return the command for the user’s editor, as a list of strings, or an empty list if none is found.
112
+ The editor is chosen from, in order:
113
+
114
+ 1. The ``editor`` argument, as a command string like ``"code --new-window"`` or a list of arguments.
115
+ 2. The ``LAUNCH_EDITOR`` environment variable.
116
+ 3. A supported editor that is currently running, on macOS, Linux, and Windows.
117
+ 4. The ``VISUAL`` environment variable.
118
+ 5. The ``EDITOR`` environment variable.
119
+
120
+ ``LAUNCH_EDITOR`` is shared with the launch-editor npm package, so you can set it once for both.
121
+ Like there, it’s treated as a single command, not split into arguments.
122
+ It may name a script, which is passed the file, line, and column as separate arguments, unless it’s a supported editor.
123
+
124
+ ``position_args(editor, filename, line, column=None)``
125
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
126
+
127
+ Return the arguments that open ``filename`` at ``line`` and ``column`` in the given ``editor`` command.
128
+ Unsupported editors get only the filename, since passing a position incorrectly can cause errors or confusing behaviour.
129
+
130
+ ``LaunchEditorError``
131
+ ~~~~~~~~~~~~~~~~~~~~~
132
+
133
+ A basic exception subclass, raised when the editor cannot be launched.
134
+
135
+ Supported editors
136
+ -----------------
137
+
138
+ Opening at a line and column is supported for:
139
+
140
+ * VS Code and its forks: Antigravity, Cursor, Trae, VSCodium
141
+ * JetBrains IDEs: AppCode, CLion, GoLand, IntelliJ IDEA, PhpStorm, PyCharm, Rider, RubyMine, WebStorm
142
+ * Atom, Emacs, Helix, joe, micro, nano, Neovim, Notepad++, Sublime Text, TextMate, Vim, Zed
143
+
144
+ Command line
145
+ ------------
146
+
147
+ Run ``python -m launch_editor`` to open a file from the command line, or check which editor would be used:
148
+
149
+ .. code-block:: console
150
+
151
+ $ python -m launch_editor --guess
152
+ code
153
+ $ python -m launch_editor src/example.py:12:4
154
+
155
+ Full help:
156
+
157
+ .. [[[cog
158
+ .. import cog
159
+ .. import os
160
+ .. import subprocess
161
+ .. import sys
162
+ .. result = subprocess.run(
163
+ .. [sys.executable, "-m", "launch_editor", "--help"],
164
+ .. capture_output=True,
165
+ .. text=True,
166
+ .. env={**os.environ, "COLUMNS": "80"},
167
+ .. )
168
+ .. cog.outl("")
169
+ .. cog.outl(".. code-block:: console")
170
+ .. cog.outl("")
171
+ .. for line in result.stdout.splitlines():
172
+ .. if line.strip() == "":
173
+ .. cog.outl("")
174
+ .. else:
175
+ .. cog.outl(" " + line.rstrip())
176
+ .. cog.outl("")
177
+ .. ]]]
178
+
179
+ .. code-block:: console
180
+
181
+ usage: python -m launch_editor [-h] [--editor EDITOR] [--guess] [FILE]
182
+
183
+ Open a file in your editor.
184
+
185
+ positional arguments:
186
+ FILE The file to open, optionally followed by :LINE or
187
+ :LINE:COLUMN, like example.py:12:4.
188
+
189
+ options:
190
+ -h, --help show this help message and exit
191
+ --editor EDITOR The editor command to use, instead of guessing.
192
+ --guess Print the editor command that would be used, and exit.
193
+
194
+ .. [[[end]]]
195
+
196
+ Security
197
+ --------
198
+
199
+ Web applications often use this package to open files from a browser, via an endpoint on a local development server.
200
+ Such an endpoint runs a program, so protect it:
201
+
202
+ * Accept an identifier for a file the application already knows about, rather than an arbitrary path.
203
+
204
+ * Only accept POST requests, and check the ``Sec-Fetch-Site`` and ``Origin`` headers to reject cross-origin requests.
205
+ Any website the user visits can send requests to ``localhost``.
206
+
207
+ * Check the ``Host`` header is a local name, like ``localhost`` or ``127.0.0.1``, to prevent DNS rebinding attacks.
208
+
209
+ Filenames are passed to editors as absolute paths, so a name starting with ``-`` cannot be interpreted as an option.
210
+
211
+ On Windows, editors launched through batch files, such as VS Code’s ``code.cmd``, run through ``cmd.exe``, which has its own command line parsing.
212
+ To prevent command injection, launch-editor-py quotes every argument and refuses to pass arguments containing ``"``, ``%``, ``!``, or line breaks to them.
213
+ UNC paths are also refused on Windows, like the npm package, to avoid leaking NTLM credentials to remote hosts—see `its explanation <https://github.com/vitejs/launch-editor/tree/main/packages/launch-editor#unc-paths-on-windows>`__.
214
+
215
+ Differences from the launch-editor npm package
216
+ ----------------------------------------------
217
+
218
+ This package was last compared against the npm package at version `2.14.2 <https://github.com/vitejs/launch-editor/tree/v2.14.2>`__ (commit ``b533489``), on 2026-10-28.
219
+ Upstream ``main`` had no further changes to the package at that time.
220
+
221
+ It also includes the fix from upstream `PR #163 <https://github.com/vitejs/launch-editor/pull/163>`__, unmerged at that time, so Cursor, Trae, and Antigravity open at the given line and column when detected running on Windows.
222
+
223
+ Additions:
224
+
225
+ * Opening at a line and column for Helix (``hx``), micro, nano, Neovim (``nvim``), and vi.
226
+
227
+ * Helix, micro, Neovim, and vi are treated as terminal editors, alongside Emacs, nano, and Vim.
228
+
229
+ * A command line interface, ``python -m launch_editor``.
230
+
231
+ Changes:
232
+
233
+ * Errors raise ``LaunchEditorError``, rather than calling an error callback and printing to the console.
234
+ A missing file is an error, rather than being silently ignored.
235
+ A GUI editor that exits with an error logs a warning to the ``launch_editor`` logger.
236
+
237
+ * ``launch_editor()`` takes the line and column as separate arguments, rather than parsing them from ``file:line:column``.
238
+ The command line interface still accepts ``file:line:column``, but not ``file://`` URLs.
239
+
240
+ * Opening a terminal editor while one is already open raises an error, rather than killing the open editor.
241
+
242
+ * ``VISUAL`` and ``EDITOR`` are split into arguments, so values like ``code --wait`` work.
243
+ ``LAUNCH_EDITOR`` is still treated as a single command, like the npm package.
244
+
245
+ * On Linux, running editors are detected by exact process name, rather than by substring, so for example ``nvim`` isn’t detected as ``vim``.
246
+
247
+ * On Windows, editor executables are run directly, rather than through ``cmd.exe``.
248
+ Batch file editors, like VS Code’s ``code.cmd``, are still run through ``cmd.exe``, with every argument quoted, rather than escaped with ``^``.
249
+ Arguments containing ``"``, ``%``, ``!``, or line breaks are refused for them, where the npm package refuses only ``%`` and line breaks.
250
+
251
+ * There’s no special handling for StackBlitz WebContainers.
252
+
253
+ License
254
+ -------
255
+
256
+ MIT.
257
+ This package is a port of the launch-editor npm package, Copyright (c) 2017-present Yuxi (Evan) You, which in turn derives from code in create-react-app, Copyright (c) 2015-present Facebook, Inc., both MIT licensed.
258
+ See the ``LICENSE`` file for details.
@@ -0,0 +1,232 @@
1
+ ================
2
+ launch-editor-py
3
+ ================
4
+
5
+ .. image:: https://img.shields.io/github/actions/workflow/status/adamchainz/launch-editor-py/main.yml.svg?branch=main&style=for-the-badge
6
+ :target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
7
+
8
+ .. image:: https://img.shields.io/badge/Coverage-100%25-success?style=for-the-badge
9
+ :target: https://github.com/adamchainz/launch-editor-py/actions?workflow=CI
10
+
11
+ .. image:: https://img.shields.io/pypi/v/launch-editor-py.svg?style=for-the-badge
12
+ :target: https://pypi.org/project/launch-editor-py/
13
+
14
+ .. image:: https://img.shields.io/badge/code%20style-black-000000.svg?style=for-the-badge
15
+ :target: https://github.com/psf/black
16
+
17
+ .. image:: https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white&style=for-the-badge
18
+ :target: https://github.com/pre-commit/pre-commit
19
+ :alt: pre-commit
20
+
21
+ ----
22
+
23
+ Open files in the user’s editor, optionally at a given line and column.
24
+
25
+ A Python port of the `launch-editor <https://github.com/vitejs/launch-editor>`__ npm package, as used by Vite to open files from the browser.
26
+ It detects which editor is running, and knows how to pass a line and column to many editors.
27
+
28
+ ----
29
+
30
+ **Get better at command line Git** with my book `Boost Your Git DX <https://adamchainz.gumroad.com/l/bygdx>`__.
31
+
32
+ ----
33
+
34
+ Requirements
35
+ ------------
36
+
37
+ Python 3.11 to 3.15 supported.
38
+
39
+ Installation
40
+ ------------
41
+
42
+ 1. Install with **pip**:
43
+
44
+ .. code-block:: sh
45
+
46
+ python -m pip install launch-editor-py
47
+
48
+ The package imports as ``launch_editor``.
49
+
50
+ Usage
51
+ -----
52
+
53
+ Call ``launch_editor()`` with a filename, and optionally a line and column:
54
+
55
+ .. code-block:: python
56
+
57
+ from launch_editor import LaunchEditorError, launch_editor
58
+
59
+ try:
60
+ launch_editor("src/example.py", line=12, column=4)
61
+ except LaunchEditorError as exc:
62
+ print(f"Could not open editor: {exc}")
63
+
64
+ API
65
+ ---
66
+
67
+ ``launch_editor(filename, line=None, column=None, *, editor=None)``
68
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
69
+
70
+ Open ``filename`` in the user’s editor, at ``line`` and ``column`` if given, and return the editor’s ``subprocess.Popen`` object.
71
+
72
+ The editor is picked by ``guess_editor()``, passing it ``editor``.
73
+
74
+ GUI editors start in the background, and the function returns immediately.
75
+ If a GUI editor exits with an error, a warning is logged to the ``launch_editor`` logger.
76
+
77
+ Terminal editors, like Vim, take over the current terminal until they exit.
78
+ While one is open, further attempts to open a terminal editor raise ``LaunchEditorError``.
79
+
80
+ Raises ``LaunchEditorError`` if the file does not exist, no editor is found, or the editor cannot be run.
81
+
82
+ ``guess_editor(editor=None)``
83
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
84
+
85
+ Return the command for the user’s editor, as a list of strings, or an empty list if none is found.
86
+ The editor is chosen from, in order:
87
+
88
+ 1. The ``editor`` argument, as a command string like ``"code --new-window"`` or a list of arguments.
89
+ 2. The ``LAUNCH_EDITOR`` environment variable.
90
+ 3. A supported editor that is currently running, on macOS, Linux, and Windows.
91
+ 4. The ``VISUAL`` environment variable.
92
+ 5. The ``EDITOR`` environment variable.
93
+
94
+ ``LAUNCH_EDITOR`` is shared with the launch-editor npm package, so you can set it once for both.
95
+ Like there, it’s treated as a single command, not split into arguments.
96
+ It may name a script, which is passed the file, line, and column as separate arguments, unless it’s a supported editor.
97
+
98
+ ``position_args(editor, filename, line, column=None)``
99
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
100
+
101
+ Return the arguments that open ``filename`` at ``line`` and ``column`` in the given ``editor`` command.
102
+ Unsupported editors get only the filename, since passing a position incorrectly can cause errors or confusing behaviour.
103
+
104
+ ``LaunchEditorError``
105
+ ~~~~~~~~~~~~~~~~~~~~~
106
+
107
+ A basic exception subclass, raised when the editor cannot be launched.
108
+
109
+ Supported editors
110
+ -----------------
111
+
112
+ Opening at a line and column is supported for:
113
+
114
+ * VS Code and its forks: Antigravity, Cursor, Trae, VSCodium
115
+ * JetBrains IDEs: AppCode, CLion, GoLand, IntelliJ IDEA, PhpStorm, PyCharm, Rider, RubyMine, WebStorm
116
+ * Atom, Emacs, Helix, joe, micro, nano, Neovim, Notepad++, Sublime Text, TextMate, Vim, Zed
117
+
118
+ Command line
119
+ ------------
120
+
121
+ Run ``python -m launch_editor`` to open a file from the command line, or check which editor would be used:
122
+
123
+ .. code-block:: console
124
+
125
+ $ python -m launch_editor --guess
126
+ code
127
+ $ python -m launch_editor src/example.py:12:4
128
+
129
+ Full help:
130
+
131
+ .. [[[cog
132
+ .. import cog
133
+ .. import os
134
+ .. import subprocess
135
+ .. import sys
136
+ .. result = subprocess.run(
137
+ .. [sys.executable, "-m", "launch_editor", "--help"],
138
+ .. capture_output=True,
139
+ .. text=True,
140
+ .. env={**os.environ, "COLUMNS": "80"},
141
+ .. )
142
+ .. cog.outl("")
143
+ .. cog.outl(".. code-block:: console")
144
+ .. cog.outl("")
145
+ .. for line in result.stdout.splitlines():
146
+ .. if line.strip() == "":
147
+ .. cog.outl("")
148
+ .. else:
149
+ .. cog.outl(" " + line.rstrip())
150
+ .. cog.outl("")
151
+ .. ]]]
152
+
153
+ .. code-block:: console
154
+
155
+ usage: python -m launch_editor [-h] [--editor EDITOR] [--guess] [FILE]
156
+
157
+ Open a file in your editor.
158
+
159
+ positional arguments:
160
+ FILE The file to open, optionally followed by :LINE or
161
+ :LINE:COLUMN, like example.py:12:4.
162
+
163
+ options:
164
+ -h, --help show this help message and exit
165
+ --editor EDITOR The editor command to use, instead of guessing.
166
+ --guess Print the editor command that would be used, and exit.
167
+
168
+ .. [[[end]]]
169
+
170
+ Security
171
+ --------
172
+
173
+ Web applications often use this package to open files from a browser, via an endpoint on a local development server.
174
+ Such an endpoint runs a program, so protect it:
175
+
176
+ * Accept an identifier for a file the application already knows about, rather than an arbitrary path.
177
+
178
+ * Only accept POST requests, and check the ``Sec-Fetch-Site`` and ``Origin`` headers to reject cross-origin requests.
179
+ Any website the user visits can send requests to ``localhost``.
180
+
181
+ * Check the ``Host`` header is a local name, like ``localhost`` or ``127.0.0.1``, to prevent DNS rebinding attacks.
182
+
183
+ Filenames are passed to editors as absolute paths, so a name starting with ``-`` cannot be interpreted as an option.
184
+
185
+ On Windows, editors launched through batch files, such as VS Code’s ``code.cmd``, run through ``cmd.exe``, which has its own command line parsing.
186
+ To prevent command injection, launch-editor-py quotes every argument and refuses to pass arguments containing ``"``, ``%``, ``!``, or line breaks to them.
187
+ UNC paths are also refused on Windows, like the npm package, to avoid leaking NTLM credentials to remote hosts—see `its explanation <https://github.com/vitejs/launch-editor/tree/main/packages/launch-editor#unc-paths-on-windows>`__.
188
+
189
+ Differences from the launch-editor npm package
190
+ ----------------------------------------------
191
+
192
+ This package was last compared against the npm package at version `2.14.2 <https://github.com/vitejs/launch-editor/tree/v2.14.2>`__ (commit ``b533489``), on 2026-10-28.
193
+ Upstream ``main`` had no further changes to the package at that time.
194
+
195
+ It also includes the fix from upstream `PR #163 <https://github.com/vitejs/launch-editor/pull/163>`__, unmerged at that time, so Cursor, Trae, and Antigravity open at the given line and column when detected running on Windows.
196
+
197
+ Additions:
198
+
199
+ * Opening at a line and column for Helix (``hx``), micro, nano, Neovim (``nvim``), and vi.
200
+
201
+ * Helix, micro, Neovim, and vi are treated as terminal editors, alongside Emacs, nano, and Vim.
202
+
203
+ * A command line interface, ``python -m launch_editor``.
204
+
205
+ Changes:
206
+
207
+ * Errors raise ``LaunchEditorError``, rather than calling an error callback and printing to the console.
208
+ A missing file is an error, rather than being silently ignored.
209
+ A GUI editor that exits with an error logs a warning to the ``launch_editor`` logger.
210
+
211
+ * ``launch_editor()`` takes the line and column as separate arguments, rather than parsing them from ``file:line:column``.
212
+ The command line interface still accepts ``file:line:column``, but not ``file://`` URLs.
213
+
214
+ * Opening a terminal editor while one is already open raises an error, rather than killing the open editor.
215
+
216
+ * ``VISUAL`` and ``EDITOR`` are split into arguments, so values like ``code --wait`` work.
217
+ ``LAUNCH_EDITOR`` is still treated as a single command, like the npm package.
218
+
219
+ * On Linux, running editors are detected by exact process name, rather than by substring, so for example ``nvim`` isn’t detected as ``vim``.
220
+
221
+ * On Windows, editor executables are run directly, rather than through ``cmd.exe``.
222
+ Batch file editors, like VS Code’s ``code.cmd``, are still run through ``cmd.exe``, with every argument quoted, rather than escaped with ``^``.
223
+ Arguments containing ``"``, ``%``, ``!``, or line breaks are refused for them, where the npm package refuses only ``%`` and line breaks.
224
+
225
+ * There’s no special handling for StackBlitz WebContainers.
226
+
227
+ License
228
+ -------
229
+
230
+ MIT.
231
+ This package is a port of the launch-editor npm package, Copyright (c) 2017-present Yuxi (Evan) You, which in turn derives from code in create-react-app, Copyright (c) 2015-present Facebook, Inc., both MIT licensed.
232
+ See the ``LICENSE`` file for details.
@@ -0,0 +1,117 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = ["uv-build>=0.12.1,<0.13"]
4
+
5
+ [project]
6
+ name = "launch-editor-py"
7
+ version = "1.0.0"
8
+ description = "Open files in the user’s editor, optionally at a given line and column."
9
+ readme = "README.rst"
10
+ keywords = [
11
+ "editor",
12
+ "launch-editor",
13
+ "open-in-editor",
14
+ ]
15
+ license = "MIT"
16
+ license-files = ["LICENSE"]
17
+ requires-python = ">=3.11"
18
+ classifiers = [
19
+ "Development Status :: 5 - Production/Stable",
20
+ "Intended Audience :: Developers",
21
+ "Natural Language :: English",
22
+ "Operating System :: OS Independent",
23
+ "Programming Language :: Python :: 3 :: Only",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Programming Language :: Python :: 3.13",
27
+ "Programming Language :: Python :: 3.14",
28
+ "Programming Language :: Python :: 3.15",
29
+ "Typing :: Typed",
30
+ ]
31
+
32
+ [[project.authors]]
33
+ name = "Adam Johnson"
34
+ email = "me@adamj.eu"
35
+
36
+ [project.urls]
37
+ Changelog = "https://github.com/adamchainz/launch-editor-py/blob/main/CHANGELOG.rst"
38
+ Funding = "https://adamj.eu/books/"
39
+ Repository = "https://github.com/adamchainz/launch-editor-py"
40
+
41
+ [dependency-groups]
42
+ test = [
43
+ "cogapp>=3.6",
44
+ "coverage[toml]>=7.10",
45
+ "pytest>=9",
46
+ "pytest-randomly",
47
+ ]
48
+
49
+ [tool.uv.build-backend]
50
+ module-name = "launch_editor"
51
+
52
+ [tool.ruff.lint]
53
+ select = [
54
+ "B",
55
+ "C4",
56
+ "E",
57
+ "F",
58
+ "I",
59
+ "SIM",
60
+ "TID",
61
+ "UP",
62
+ "W",
63
+ ]
64
+ ignore = [
65
+ "B9",
66
+ "E501",
67
+ "SIM105",
68
+ "SIM108",
69
+ ]
70
+ extend-safe-fixes = ["UP006"]
71
+
72
+ [tool.ruff.lint.isort]
73
+ required-imports = ["from __future__ import annotations"]
74
+
75
+ [tool.pyproject-fmt]
76
+ max_supported_python = "3.15"
77
+
78
+ [tool.mypy]
79
+ mypy_path = "src/"
80
+ namespace_packages = false
81
+ warn_unreachable = true
82
+ enable_error_code = [
83
+ "ignore-without-code",
84
+ "redundant-expr",
85
+ "truthy-bool",
86
+ ]
87
+ strict = true
88
+
89
+ [[tool.mypy.overrides]]
90
+ module = "tests.*"
91
+ allow_untyped_defs = true
92
+
93
+ [tool.pytest]
94
+ strict = true
95
+
96
+ [tool.coverage.run]
97
+ source = [
98
+ "launch_editor",
99
+ "tests",
100
+ ]
101
+ branch = true
102
+ data_file = ".coverage/cov"
103
+ parallel = true
104
+
105
+ [tool.coverage.paths]
106
+ source = [
107
+ "src",
108
+ ".tox/**/site-packages",
109
+ ]
110
+
111
+ [tool.coverage.report]
112
+ skip_covered = true
113
+ skip_empty = true
114
+ show_missing = true
115
+
116
+ [tool.rstcheck]
117
+ report_level = "ERROR"
@@ -0,0 +1,123 @@
1
+ [build-system]
2
+ build-backend = "uv_build"
3
+ requires = [
4
+ "uv-build>=0.12.1,<0.13",
5
+ ]
6
+
7
+ [project]
8
+ name = "launch-editor-py"
9
+ version = "1.0.0"
10
+ description = "Open files in the user’s editor, optionally at a given line and column."
11
+ readme = "README.rst"
12
+ keywords = [
13
+ "editor",
14
+ "launch-editor",
15
+ "open-in-editor",
16
+ ]
17
+ license = "MIT"
18
+ license-files = [ "LICENSE" ]
19
+ authors = [
20
+ { name = "Adam Johnson", email = "me@adamj.eu" },
21
+ ]
22
+ requires-python = ">=3.11"
23
+ classifiers = [
24
+ "Development Status :: 5 - Production/Stable",
25
+ "Intended Audience :: Developers",
26
+ "Natural Language :: English",
27
+ "Operating System :: OS Independent",
28
+ "Programming Language :: Python :: 3 :: Only",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
33
+ "Programming Language :: Python :: 3.15",
34
+ "Typing :: Typed",
35
+ ]
36
+ urls.Changelog = "https://github.com/adamchainz/launch-editor-py/blob/main/CHANGELOG.rst"
37
+ urls.Funding = "https://adamj.eu/books/"
38
+ urls.Repository = "https://github.com/adamchainz/launch-editor-py"
39
+
40
+ [dependency-groups]
41
+ test = [
42
+ "cogapp>=3.6",
43
+ "coverage[toml]>=7.10",
44
+ "pytest>=9",
45
+ "pytest-randomly",
46
+ ]
47
+
48
+ [tool.uv]
49
+ build-backend.module-name = "launch_editor"
50
+
51
+ [tool.ruff]
52
+ lint.select = [
53
+ # flake8-bugbear
54
+ "B",
55
+ # flake8-comprehensions
56
+ "C4",
57
+ # pycodestyle
58
+ "E",
59
+ # Pyflakes errors
60
+ "F",
61
+ # isort
62
+ "I",
63
+ # flake8-simplify
64
+ "SIM",
65
+ # flake8-tidy-imports
66
+ "TID",
67
+ # pyupgrade
68
+ "UP",
69
+ # Pyflakes warnings
70
+ "W",
71
+ ]
72
+ lint.ignore = [
73
+ # flake8-bugbear opinionated rules
74
+ "B9",
75
+ # line-too-long
76
+ "E501",
77
+ # suppressible-exception
78
+ "SIM105",
79
+ # if-else-block-instead-of-if-exp
80
+ "SIM108",
81
+ ]
82
+ lint.extend-safe-fixes = [
83
+ # non-pep585-annotation
84
+ "UP006",
85
+ ]
86
+ lint.isort.required-imports = [ "from __future__ import annotations" ]
87
+
88
+ [tool.pyproject-fmt]
89
+ max_supported_python = "3.15"
90
+
91
+ [tool.mypy]
92
+ mypy_path = "src/"
93
+ namespace_packages = false
94
+ warn_unreachable = true
95
+ enable_error_code = [
96
+ "ignore-without-code",
97
+ "redundant-expr",
98
+ "truthy-bool",
99
+ ]
100
+ strict = true
101
+ overrides = [ { module = "tests.*", allow_untyped_defs = true } ]
102
+
103
+ [tool.pytest]
104
+ strict = true
105
+
106
+ [tool.coverage]
107
+ run.source = [
108
+ "launch_editor",
109
+ "tests",
110
+ ]
111
+ run.branch = true
112
+ run.data_file = ".coverage/cov"
113
+ run.parallel = true
114
+ paths.source = [
115
+ "src",
116
+ ".tox/**/site-packages",
117
+ ]
118
+ report.skip_covered = true
119
+ report.skip_empty = true
120
+ report.show_missing = true
121
+
122
+ [tool.rstcheck]
123
+ report_level = "ERROR"
@@ -0,0 +1,535 @@
1
+ """
2
+ Open files in the user’s editor, at a given line and column.
3
+
4
+ A Python port of the launch-editor npm package:
5
+ https://github.com/vitejs/launch-editor
6
+
7
+ Last compared against version 2.14.2. See “Differences from the npm package”
8
+ in README.rst for the upstream PRs included and other changes.
9
+
10
+ Copyright (c) 2026 Adam Johnson
11
+ Copyright (c) 2017-present, Yuxi (Evan) You
12
+ Copyright (c) 2015-present, Facebook, Inc.
13
+
14
+ Released under the MIT License, see the LICENSE file.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import logging
20
+ import ntpath
21
+ import os
22
+ import platform
23
+ import re
24
+ import shlex
25
+ import shutil
26
+ import subprocess
27
+ import sys
28
+ import threading
29
+ from collections.abc import Sequence
30
+ from pathlib import Path, PureWindowsPath
31
+
32
+ __all__ = [
33
+ "LaunchEditorError",
34
+ "guess_editor",
35
+ "launch_editor",
36
+ "position_args",
37
+ ]
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class LaunchEditorError(Exception):
43
+ """
44
+ Raised when the editor cannot be launched.
45
+ """
46
+
47
+
48
+ # Process names, as listed by `ps` on macOS, mapped to the command that opens
49
+ # files in that editor. Where there’s no command-line launcher, the process
50
+ # path maps to itself.
51
+ MACOS_EDITORS = {
52
+ "/Applications/Atom.app/Contents/MacOS/Atom": "atom",
53
+ "/Applications/Atom Beta.app/Contents/MacOS/Atom Beta": (
54
+ "/Applications/Atom Beta.app/Contents/MacOS/Atom Beta"
55
+ ),
56
+ "/Applications/Brackets.app/Contents/MacOS/Brackets": "brackets",
57
+ "/Applications/Sublime Text.app/Contents/MacOS/Sublime Text": (
58
+ "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"
59
+ ),
60
+ "/Applications/Sublime Text.app/Contents/MacOS/sublime_text": (
61
+ "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl"
62
+ ),
63
+ "/Applications/Sublime Text 2.app/Contents/MacOS/Sublime Text 2": (
64
+ "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"
65
+ ),
66
+ "/Applications/Sublime Text Dev.app/Contents/MacOS/Sublime Text": (
67
+ "/Applications/Sublime Text Dev.app/Contents/SharedSupport/bin/subl"
68
+ ),
69
+ "/Applications/Visual Studio Code.app/Contents/MacOS/Code": "code",
70
+ "/Applications/Visual Studio Code.app/Contents/MacOS/Electron": "code",
71
+ "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders": (
72
+ "code-insiders"
73
+ ),
74
+ "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Electron": (
75
+ "code-insiders"
76
+ ),
77
+ "/Applications/VSCodium.app/Contents/MacOS/Electron": "codium",
78
+ "/Applications/Cursor.app/Contents/MacOS/Cursor": "cursor",
79
+ "/Applications/Trae.app/Contents/MacOS/Electron": "trae",
80
+ "/Applications/Antigravity.app/Contents/MacOS/Electron": "antigravity",
81
+ "/Applications/AppCode.app/Contents/MacOS/appcode": (
82
+ "/Applications/AppCode.app/Contents/MacOS/appcode"
83
+ ),
84
+ "/Applications/CLion.app/Contents/MacOS/clion": (
85
+ "/Applications/CLion.app/Contents/MacOS/clion"
86
+ ),
87
+ "/Applications/IntelliJ IDEA.app/Contents/MacOS/idea": (
88
+ "/Applications/IntelliJ IDEA.app/Contents/MacOS/idea"
89
+ ),
90
+ "/Applications/IntelliJ IDEA Ultimate.app/Contents/MacOS/idea": (
91
+ "/Applications/IntelliJ IDEA Ultimate.app/Contents/MacOS/idea"
92
+ ),
93
+ "/Applications/IntelliJ IDEA Community Edition.app/Contents/MacOS/idea": (
94
+ "/Applications/IntelliJ IDEA Community Edition.app/Contents/MacOS/idea"
95
+ ),
96
+ "/Applications/PhpStorm.app/Contents/MacOS/phpstorm": (
97
+ "/Applications/PhpStorm.app/Contents/MacOS/phpstorm"
98
+ ),
99
+ "/Applications/PyCharm.app/Contents/MacOS/pycharm": (
100
+ "/Applications/PyCharm.app/Contents/MacOS/pycharm"
101
+ ),
102
+ "/Applications/PyCharm CE.app/Contents/MacOS/pycharm": (
103
+ "/Applications/PyCharm CE.app/Contents/MacOS/pycharm"
104
+ ),
105
+ "/Applications/RubyMine.app/Contents/MacOS/rubymine": (
106
+ "/Applications/RubyMine.app/Contents/MacOS/rubymine"
107
+ ),
108
+ "/Applications/WebStorm.app/Contents/MacOS/webstorm": (
109
+ "/Applications/WebStorm.app/Contents/MacOS/webstorm"
110
+ ),
111
+ "/Applications/MacVim.app/Contents/MacOS/MacVim": "mvim",
112
+ "/Applications/GoLand.app/Contents/MacOS/goland": (
113
+ "/Applications/GoLand.app/Contents/MacOS/goland"
114
+ ),
115
+ "/Applications/Rider.app/Contents/MacOS/rider": (
116
+ "/Applications/Rider.app/Contents/MacOS/rider"
117
+ ),
118
+ "/Applications/Zed.app/Contents/MacOS/zed": "zed",
119
+ }
120
+
121
+ # Process names, as listed by `ps` on Linux, mapped to the command that opens
122
+ # files in that editor. Order matters: earlier entries take precedence.
123
+ LINUX_EDITORS = {
124
+ "atom": "atom",
125
+ "Brackets": "brackets",
126
+ "code-insiders": "code-insiders",
127
+ "code": "code",
128
+ "vscodium": "vscodium",
129
+ "codium": "codium",
130
+ "cursor": "cursor",
131
+ "trae": "trae",
132
+ "antigravity": "antigravity",
133
+ "emacs": "emacs",
134
+ "gvim": "gvim",
135
+ "idea": "idea",
136
+ "idea.sh": "idea",
137
+ "phpstorm": "phpstorm",
138
+ "phpstorm.sh": "phpstorm",
139
+ "pycharm": "pycharm",
140
+ "pycharm.sh": "pycharm",
141
+ "rubymine": "rubymine",
142
+ "rubymine.sh": "rubymine",
143
+ "sublime_text": "subl",
144
+ "vim": "vim",
145
+ "webstorm": "webstorm",
146
+ "webstorm.sh": "webstorm",
147
+ "goland": "goland",
148
+ "goland.sh": "goland",
149
+ "rider": "rider",
150
+ "rider.sh": "rider",
151
+ "zed": "zed",
152
+ }
153
+
154
+ # Executable names of editors on Windows. A running one is launched by its
155
+ # full path.
156
+ WINDOWS_EDITORS = [
157
+ "Brackets.exe",
158
+ "Code.exe",
159
+ "Code - Insiders.exe",
160
+ "VSCodium.exe",
161
+ "Cursor.exe",
162
+ "atom.exe",
163
+ "sublime_text.exe",
164
+ "notepad++.exe",
165
+ "clion.exe",
166
+ "clion64.exe",
167
+ "idea.exe",
168
+ "idea64.exe",
169
+ "phpstorm.exe",
170
+ "phpstorm64.exe",
171
+ "pycharm.exe",
172
+ "pycharm64.exe",
173
+ "rubymine.exe",
174
+ "rubymine64.exe",
175
+ "webstorm.exe",
176
+ "webstorm64.exe",
177
+ "goland.exe",
178
+ "goland64.exe",
179
+ "rider.exe",
180
+ "rider64.exe",
181
+ "Trae.exe",
182
+ "zed.exe",
183
+ "Antigravity.exe",
184
+ ]
185
+
186
+ # Editors that run in the terminal, taking it over until they exit.
187
+ TERMINAL_EDITORS = {"emacs", "hx", "micro", "nano", "nvim", "vi", "vim"}
188
+
189
+
190
+ def guess_editor(editor: str | Sequence[str] | None = None) -> list[str]:
191
+ """
192
+ Return the command for the user’s editor, or an empty list if none is
193
+ found.
194
+
195
+ The editor is chosen from, in order:
196
+
197
+ 1. The ``editor`` argument.
198
+ 2. The ``LAUNCH_EDITOR`` environment variable.
199
+ 3. A supported editor that is currently running.
200
+ 4. The ``VISUAL`` environment variable.
201
+ 5. The ``EDITOR`` environment variable.
202
+ """
203
+ if editor:
204
+ if isinstance(editor, str):
205
+ return _split_command(editor)
206
+ return list(editor)
207
+
208
+ launch_editor_env = os.environ.get("LAUNCH_EDITOR")
209
+ if launch_editor_env:
210
+ # Like the npm package, treat this as a single command, which is often
211
+ # a script that receives the file, line, and column.
212
+ return [launch_editor_env]
213
+
214
+ running = _guess_running_editor()
215
+ if running is not None:
216
+ return [running]
217
+
218
+ for var in ("VISUAL", "EDITOR"):
219
+ value = os.environ.get(var)
220
+ if value:
221
+ return _split_command(value)
222
+
223
+ return []
224
+
225
+
226
+ def _split_command(command: str) -> list[str]:
227
+ return shlex.split(command, posix=(sys.platform != "win32"))
228
+
229
+
230
+ def _guess_running_editor() -> str | None:
231
+ if sys.platform == "darwin":
232
+ args = ["ps", "x", "-o", "comm="]
233
+ parse = _editor_from_macos_processes
234
+ elif sys.platform == "win32":
235
+ args = [
236
+ "powershell",
237
+ "-NoProfile",
238
+ "-Command",
239
+ (
240
+ "[Console]::OutputEncoding=[Text.Encoding]::UTF8;"
241
+ + "Get-CimInstance -Query "
242
+ + '"select executablepath from win32_process '
243
+ + 'where executablepath is not null" '
244
+ + "| % { $_.ExecutablePath }"
245
+ ),
246
+ ]
247
+ parse = _editor_from_windows_processes
248
+ elif sys.platform == "linux":
249
+ args = ["ps", "x", "--no-heading", "-o", "comm", "--sort=comm"]
250
+ parse = _editor_from_linux_processes
251
+ else:
252
+ return None
253
+
254
+ try:
255
+ output = subprocess.run(
256
+ args,
257
+ stdin=subprocess.DEVNULL,
258
+ stderr=subprocess.DEVNULL,
259
+ stdout=subprocess.PIPE,
260
+ encoding="utf-8",
261
+ errors="replace",
262
+ check=True,
263
+ ).stdout
264
+ except (OSError, subprocess.CalledProcessError):
265
+ return None
266
+
267
+ return parse(output)
268
+
269
+
270
+ def _editor_from_macos_processes(output: str) -> str | None:
271
+ processes = output.splitlines()
272
+ for process_name, command in MACOS_EDITORS.items():
273
+ if process_name in processes:
274
+ return command
275
+ # Find editors installed outside /Applications.
276
+ suffix = process_name.removeprefix("/Applications")
277
+ if suffix in output:
278
+ # Use the command-line launcher, if there is one.
279
+ if command != process_name:
280
+ return command
281
+ # Otherwise, use the running process’ path.
282
+ for process in processes:
283
+ if process.endswith(suffix):
284
+ return process
285
+ return None
286
+
287
+
288
+ def _editor_from_windows_processes(output: str) -> str | None:
289
+ for line in output.splitlines():
290
+ path = line.strip()
291
+ if PureWindowsPath(path).name in WINDOWS_EDITORS:
292
+ return path
293
+ return None
294
+
295
+
296
+ def _editor_from_linux_processes(output: str) -> str | None:
297
+ processes = set(output.split())
298
+ for process_name, command in LINUX_EDITORS.items():
299
+ if process_name in processes:
300
+ return command
301
+ return None
302
+
303
+
304
+ # Editor names, grouped by how they take a position.
305
+ _COLON_POSITION_EDITORS = {
306
+ "Atom",
307
+ "Atom Beta",
308
+ "atom",
309
+ "charm",
310
+ "hx",
311
+ "subl",
312
+ "sublime",
313
+ "sublime_text",
314
+ "wstorm",
315
+ "zed",
316
+ }
317
+ _VSCODE_LIKE_EDITORS = {
318
+ "Antigravity",
319
+ "Code",
320
+ "Code - Insiders",
321
+ "Cursor",
322
+ "Trae",
323
+ "VSCodium",
324
+ "antigravity",
325
+ "code",
326
+ "code-insiders",
327
+ "codium",
328
+ "cursor",
329
+ "trae",
330
+ "vscodium",
331
+ }
332
+ _JETBRAINS_EDITORS = {
333
+ "appcode",
334
+ "clion",
335
+ "clion64",
336
+ "goland",
337
+ "goland64",
338
+ "idea",
339
+ "idea64",
340
+ "phpstorm",
341
+ "phpstorm64",
342
+ "pycharm",
343
+ "pycharm64",
344
+ "rider",
345
+ "rider64",
346
+ "rubymine",
347
+ "rubymine64",
348
+ "webstorm",
349
+ "webstorm64",
350
+ }
351
+
352
+
353
+ def position_args(
354
+ editor: str, filename: str, line: int, column: int | None = None
355
+ ) -> list[str]:
356
+ """
357
+ Return the arguments to open ``filename`` at ``line`` and ``column`` in
358
+ the given editor command.
359
+ """
360
+ if column is None:
361
+ column = 1
362
+ name = PureWindowsPath(editor).name if "\\" in editor else Path(editor).name
363
+ name = re.sub(r"\.(exe|cmd|bat)$", "", name, flags=re.IGNORECASE)
364
+
365
+ if name in _COLON_POSITION_EDITORS:
366
+ return [f"{filename}:{line}:{column}"]
367
+ if name == "notepad++":
368
+ return [f"-n{line}", f"-c{column}", filename]
369
+ if name in {"vim", "mvim", "nvim"}:
370
+ return [f"+call cursor({line}, {column})", filename]
371
+ if name in {"joe", "gvim", "vi"}:
372
+ return [f"+{line}", filename]
373
+ if name in {"emacs", "emacsclient", "micro"}:
374
+ return [f"+{line}:{column}", filename]
375
+ if name == "nano":
376
+ return [f"+{line},{column}", filename]
377
+ if name in {"rmate", "mate", "mine"}:
378
+ return ["--line", str(line), filename]
379
+ if name in _VSCODE_LIKE_EDITORS:
380
+ return ["-r", "-g", f"{filename}:{line}:{column}"]
381
+ if name in _JETBRAINS_EDITORS:
382
+ return ["--line", str(line), "--column", str(column), filename]
383
+
384
+ if os.environ.get("LAUNCH_EDITOR"):
385
+ return [filename, str(line), str(column)]
386
+
387
+ # For other editors, drop the position, since passing it incorrectly can
388
+ # result in errors or confusing behaviour.
389
+ return [filename]
390
+
391
+
392
+ _terminal_editor: subprocess.Popen[bytes] | None = None
393
+ _terminal_editor_lock = threading.Lock()
394
+
395
+
396
+ def launch_editor(
397
+ filename: str | os.PathLike[str],
398
+ line: int | None = None,
399
+ column: int | None = None,
400
+ *,
401
+ editor: str | Sequence[str] | None = None,
402
+ ) -> subprocess.Popen[bytes]:
403
+ """
404
+ Open ``filename`` in the user’s editor, at ``line`` and ``column`` if
405
+ given.
406
+
407
+ The editor is picked by :func:`guess_editor`, using ``editor`` if given.
408
+ GUI editors are started in the background and this function returns
409
+ immediately. Terminal editors, like Vim, take over the current terminal;
410
+ while one is open, further calls to open a terminal editor fail.
411
+
412
+ Returns the editor process. Raises :class:`LaunchEditorError` if the
413
+ editor cannot be launched.
414
+ """
415
+ global _terminal_editor
416
+
417
+ filename = os.fspath(filename)
418
+
419
+ if sys.platform == "win32" and ntpath.abspath(filename).startswith("\\\\"):
420
+ raise LaunchEditorError(
421
+ f"Cannot open {filename}: UNC paths are not supported on Windows to "
422
+ + "avoid security issues. See "
423
+ + "https://github.com/vitejs/launch-editor/tree/main/packages/"
424
+ + "launch-editor#unc-paths-on-windows for details."
425
+ )
426
+
427
+ # Like the npm package, use an absolute path, so a filename starting with
428
+ # "-" cannot be interpreted as an option by the editor.
429
+ filename = os.path.abspath(filename)
430
+
431
+ if not os.path.exists(filename):
432
+ raise LaunchEditorError(f"File does not exist: {filename}")
433
+
434
+ command = guess_editor(editor)
435
+ if not command:
436
+ raise LaunchEditorError(
437
+ "Could not find an editor. Set the LAUNCH_EDITOR, VISUAL, or EDITOR "
438
+ + "environment variable."
439
+ )
440
+ editor_command, *args = command
441
+
442
+ if (
443
+ sys.platform == "linux"
444
+ and filename.startswith("/mnt/")
445
+ and "microsoft" in platform.release().lower()
446
+ ):
447
+ # Assume WSL is being used, and that the file is on the Windows file
448
+ # system. Windows editors can translate the path, but only if it is
449
+ # relative. Prefix it with "./" so it cannot be interpreted as an
450
+ # option.
451
+ filename = "./" + os.path.relpath(filename)
452
+
453
+ if line is not None:
454
+ args += position_args(editor_command, filename, line, column)
455
+ else:
456
+ args.append(filename)
457
+
458
+ with _terminal_editor_lock:
459
+ is_terminal = Path(editor_command).name in TERMINAL_EDITORS
460
+ if is_terminal and _terminal_editor is not None:
461
+ if _terminal_editor.poll() is None:
462
+ raise LaunchEditorError(
463
+ f"{Path(editor_command).name} is already open in the "
464
+ + "terminal. Close it first."
465
+ )
466
+ _terminal_editor = None
467
+
468
+ process = _run(editor_command, args)
469
+
470
+ if is_terminal:
471
+ _terminal_editor = process
472
+
473
+ threading.Thread(
474
+ name=f"launch_editor-wait-{process.pid}",
475
+ target=_report_failure,
476
+ args=(process, editor_command, filename),
477
+ daemon=True,
478
+ ).start()
479
+ return process
480
+
481
+
482
+ def _run(editor: str, args: list[str]) -> subprocess.Popen[bytes]:
483
+ executable = shutil.which(editor) or editor
484
+ try:
485
+ if sys.platform == "win32" and executable.lower().endswith((".cmd", ".bat")):
486
+ # Windows runs batch files, such as VS Code’s code.cmd, with
487
+ # cmd.exe, which applies its own parsing to the command line. Quote
488
+ # every argument, and reject characters that cmd.exe would still
489
+ # interpret inside quotes, to prevent command injection.
490
+ return subprocess.Popen(_cmd_command_line([executable, *args]))
491
+ return subprocess.Popen([executable, *args])
492
+ except FileNotFoundError as exc:
493
+ raise LaunchEditorError(
494
+ f"Could not run {editor!r}: command not found on PATH."
495
+ ) from exc
496
+ except OSError as exc:
497
+ raise LaunchEditorError(f"Could not run {editor!r}: {exc}") from exc
498
+
499
+
500
+ _CMD_UNSAFE_CHARS = {
501
+ '"': "a double quote",
502
+ "%": 'a percent sign ("%")',
503
+ "!": 'an exclamation mark ("!")',
504
+ "\r": "a line break",
505
+ "\n": "a line break",
506
+ }
507
+
508
+
509
+ def _cmd_command_line(args: list[str]) -> str:
510
+ for arg in args:
511
+ for char, description in _CMD_UNSAFE_CHARS.items():
512
+ if char in arg:
513
+ raise LaunchEditorError(
514
+ f"Cannot safely pass {arg!r} to {args[0]!r}: it contains "
515
+ + f"{description}, which cmd.exe would interpret, allowing "
516
+ + "command injection. Use an editor that is an executable "
517
+ + "rather than a batch file, or rename the file. See "
518
+ + "https://github.com/vitejs/launch-editor/blob/v2.14.2/"
519
+ + "packages/launch-editor/construct-windows-launch-command.js "
520
+ + "for details."
521
+ )
522
+ return " ".join(f'"{arg}"' for arg in args)
523
+
524
+
525
+ def _report_failure(
526
+ process: subprocess.Popen[bytes], editor: str, filename: str
527
+ ) -> None:
528
+ returncode = process.wait()
529
+ if returncode:
530
+ logger.warning(
531
+ "Could not open %s in the editor: %r exited with code %d.",
532
+ os.path.basename(filename),
533
+ editor,
534
+ returncode,
535
+ )
@@ -0,0 +1,71 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import re
5
+ import shlex
6
+ import sys
7
+ from collections.abc import Sequence
8
+
9
+ from launch_editor import LaunchEditorError, guess_editor, launch_editor
10
+
11
+ _POSITION_RE = re.compile(r":(\d+)(?::(\d+))?$")
12
+
13
+
14
+ def main(argv: Sequence[str] | None = None) -> int:
15
+ parser = argparse.ArgumentParser(
16
+ prog="python -m launch_editor",
17
+ description="Open a file in your editor.",
18
+ allow_abbrev=False,
19
+ )
20
+ parser.add_argument(
21
+ "file",
22
+ metavar="FILE",
23
+ nargs="?",
24
+ help=(
25
+ "The file to open, optionally followed by :LINE or :LINE:COLUMN, "
26
+ + "like example.py:12:4."
27
+ ),
28
+ )
29
+ parser.add_argument(
30
+ "--editor",
31
+ help="The editor command to use, instead of guessing.",
32
+ )
33
+ parser.add_argument(
34
+ "--guess",
35
+ action="store_true",
36
+ help="Print the editor command that would be used, and exit.",
37
+ )
38
+ args = parser.parse_args(argv)
39
+
40
+ if args.guess:
41
+ command = guess_editor(args.editor)
42
+ if not command:
43
+ print("No editor found.", file=sys.stderr)
44
+ return 1
45
+ print(shlex.join(command))
46
+ return 0
47
+
48
+ if args.file is None:
49
+ parser.error("FILE is required.")
50
+
51
+ filename = args.file
52
+ line = column = None
53
+ match = _POSITION_RE.search(filename)
54
+ if match:
55
+ filename = filename[: match.start()]
56
+ line = int(match[1])
57
+ if match[2] is not None:
58
+ column = int(match[2])
59
+
60
+ try:
61
+ process = launch_editor(filename, line, column, editor=args.editor)
62
+ except LaunchEditorError as exc:
63
+ print(exc, file=sys.stderr)
64
+ return 1
65
+
66
+ # Wait, so terminal editors keep the terminal, and to report failures.
67
+ return process.wait()
68
+
69
+
70
+ if __name__ == "__main__": # pragma: no cover
71
+ raise SystemExit(main())
File without changes