-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathopen_in_editor.py
executable file
·267 lines (208 loc) · 7.75 KB
/
open_in_editor.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
#!/usr/bin/env python3
'''
This scripts allows opening your text editor from a link on a webpage/within a browser extension via MIME.
See a short [[https://karlicoss.github.io/promnesia-demos/jump_to_editor.webm][demo]].
It handles URIs like:
: editor:///path/to/file:123
: editor:///path/to/file?line=456
See =test_parse_uri= for more examples.
To install (register the MIME handler), run
: python3 open_in_editor.py --install --editor emacs
See =--help= for the list of available editors. If you want to add other editors, the code should be easy to follow.
You can check that it works with
: xdg-open 'editor:///path/to/some/file'
I haven't found any existing/mature scripts for this, *please let me know if you know of any*! I'd be quite happy to support one less script :)
The script was tested on *Linux only*! I'd be happy if someone contributes adjustments for OSX.
'''
# TODO make it editor-agnostic? although supporting line numbers will be trickier
PROTOCOL_NAME = 'editor'
def test_open_editor():
import time
from tempfile import TemporaryDirectory
with TemporaryDirectory() as td:
p = Path(td) / 'some file.org'
p.write_text('''
line 1
line 2
line 3 ---- THIS LINE SHOULD BE IN FOCUS!
line 4
'''.strip())
# todo eh, warns about swapfile
for editor in EDITORS.keys():
open_editor(f'editor://{p}:3', editor=editor)
input("Press enter when ready")
import argparse
from pathlib import Path
import sys
import subprocess
from subprocess import check_call, run
import tempfile
from urllib.parse import unquote, urlparse, parse_qsl
def notify(what) -> None:
# notify-send used as a user-facing means of error reporting
run(["notify-send", what])
def error(what) -> None:
notify(what)
raise RuntimeError(what)
def install(editor: str) -> None:
this_script = str(Path(__file__).absolute())
CONTENT = f"""
[Desktop Entry]
Name=Open file in your text editor
Exec=python3 {this_script} --editor {editor} %u
Type=Application
Terminal=false
MimeType=x-scheme-handler/{PROTOCOL_NAME};
""".strip()
with tempfile.TemporaryDirectory() as td:
pp = Path(td) / 'open_in_editor.desktop'
pp.write_text(CONTENT)
check_call(['desktop-file-validate', str(pp)])
dfile = Path('~/.local/share/applications').expanduser()
check_call([
'desktop-file-install',
'--dir', dfile,
'--rebuild-mime-info-cache',
str(pp),
])
print(f"Installed {pp.name} file to {dfile}", file=sys.stderr)
print(f"""You might want to check if it works with "xdg-open '{PROTOCOL_NAME}:///path/to/some/file'" """, file=sys.stderr)
from typing import Tuple, Optional, List
Line = int
File = str
def parse_uri(uri: str) -> Tuple[File, Optional[Line]]:
"""
>>> parse_uri('editor:///path/to/file')
('/path/to/file', None)
>>> parse_uri("editor:///path/with spaces")
('/path/with spaces', None)
>>> parse_uri("editor:///path/url%20encoded")
('/path/url encoded', None)
# TODO not sure about whether this or lien= thing is preferrable
>>> parse_uri("editor:///path/to/file:10")
('/path/to/file', 10)
# todo not sure about this. I guess it's a more 'proper' way? non ambiguous and supports columns and other stuff potentially
>>> parse_uri("editor:///path/to/file?line=10")
('/path/to/file', 10)
>>> parse_uri("editor:///path/to/file:oops/and:more")
('/path/to/file:oops/and:more', None)
>>> parse_uri('badmime://whatever')
Traceback (most recent call last):
RuntimeError: Unexpected protocol badmime://whatever
"""
pr = urlparse(uri)
if pr.scheme != PROTOCOL_NAME:
error(f"Unexpected protocol {uri}")
# not sure if a good idea to keep trying nevertheless?
path = unquote(pr.path)
linenum: Optional[int] = None
line_s = dict(parse_qsl(pr.query)).get('line', None)
if line_s is not None:
linenum = int(line_s)
else:
spl = path.rsplit(':', maxsplit=1)
# meh. not sure about this
if len(spl) == 2:
try:
linenum = int(spl[1])
except ValueError:
# eh. presumably just a colon in filename
pass
else:
path = spl[0]
return (path, linenum)
def open_editor(uri: str, editor: str) -> None:
uri, line = parse_uri(uri)
# TODO seems that sublime and atom support :line:column syntax? not sure how to pass it through xdg-open though
opener = EDITORS.get(editor, None)
if opener is None:
notify(f'Unexpected editor {editor}! Falling back to vim')
opener = open_vim
opener(uri, line)
def with_line(uri: File, line: Optional[Line]) -> List[str]:
return [uri] if line is None else [f'+{line}', uri]
def open_default(uri: File, line:Optional[Line]) -> None:
import shutil
for open_cmd in ['xdg-open', 'open']:
if shutil.which(open_cmd):
# sadly no generic way to handle line for editors?
check_call([open_cmd, uri])
break
else:
error("No xdg-open/open found, can't figure out default editor. Fallback to vim!")
open_vim(uri=uri, line=line)
def open_gvim(uri: File, line: Optional[Line]) -> None:
args = with_line(uri, line)
cmd = [
'gvim',
*args,
]
check_call(['gvim', *args])
def open_kwrite(uri: File, line: Optional[Line]) -> None:
check_call(['kwrite'] + (['--line', str(line)] if line else []) + [uri])
def open_vim(uri: File, line: Optional[Line]) -> None:
args = with_line(uri, line)
launch_in_terminal(['vim', *args])
def open_vscode(uri: File, line: Optional[Line]) -> None:
if line:
args = ['--goto', f'{uri}:{line}']
else:
args = [uri]
check_call(['code', *args])
def open_emacs(uri: File, line: Optional[Line]) -> None:
args = with_line(uri, line)
cmd = [
'emacsclient',
'--create-frame',
# trick to run daemon if it isn't https://www.gnu.org/software/emacs/manual/html_node/emacs/emacsclient-Options.html # doesn't work in 27.1?
'--alternate-editor=""',
*args,
]
# todo exec?
check_call(cmd)
return
### alternatively, if you prefer a terminal emacs
cmd = [
'emacsclient',
'--tty',
'--alternate-editor=""',
*args,
]
launch_in_terminal(cmd)
###
EDITORS = {
'emacs' : open_emacs,
'vim' : open_vim,
'gvim' : open_gvim,
'vscode' : open_vscode,
'default': open_default,
'kwrite' : open_kwrite,
}
def launch_in_terminal(cmd: List[str]):
import shlex
check_call([
# NOTE: you might need xdg-terminal on some systems
"x-terminal-emulator",
"-e",
' '.join(map(shlex.quote, cmd)),
])
# TODO could use that for column number? maybe an overkill though.. and most extractors won'tsupport it anyway
# https://www.gnu.org/software/emacs/manual/html_node/emacs/emacsclient-Options.html
def main():
p = argparse.ArgumentParser()
# TODO allow passing a binary?
p.add_argument('--editor', type=str, default='vim', choices=EDITORS.keys(), help="Editor to use. 'default' means using your default GUI editor (discovered with open/xdg-open)")
p.add_argument('--install', action='store_true', help='Pass to install (i.g. register MIME in your system)')
p.add_argument('uri', nargs='?', help='URI to open + optional line number')
p.add_argument('--run-tests', action='store_true', help='Pass to run unit tests')
args = p.parse_args()
if args.run_tests:
import doctest
doctest.testmod()
test_open_editor()
elif args.install:
install(editor=args.editor)
else:
open_editor(args.uri, editor=args.editor)
if __name__ == '__main__':
main()