Skip to content

Commit 5dc4296

Browse files
committed
0 parents  commit 5dc4296

File tree

2 files changed

+387
-0
lines changed

2 files changed

+387
-0
lines changed

.gitignore

+160
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
2+
# Created by https://www.gitignore.io/api/python,emacs
3+
# Edit at https://www.gitignore.io/?templates=python,emacs
4+
5+
### Emacs ###
6+
# -*- mode: gitignore; -*-
7+
*~
8+
\#*\#
9+
/.emacs.desktop
10+
/.emacs.desktop.lock
11+
*.elc
12+
auto-save-list
13+
tramp
14+
.\#*
15+
16+
# Org-mode
17+
.org-id-locations
18+
*_archive
19+
20+
# flymake-mode
21+
*_flymake.*
22+
23+
# eshell files
24+
/eshell/history
25+
/eshell/lastdir
26+
27+
# elpa packages
28+
/elpa/
29+
30+
# reftex files
31+
*.rel
32+
33+
# AUCTeX auto folder
34+
/auto/
35+
36+
# cask packages
37+
.cask/
38+
dist/
39+
40+
# Flycheck
41+
flycheck_*.el
42+
43+
# server auth directory
44+
/server/
45+
46+
# projectiles files
47+
.projectile
48+
49+
# directory configuration
50+
.dir-locals.el
51+
52+
# network security
53+
/network-security.data
54+
55+
56+
### Python ###
57+
# Byte-compiled / optimized / DLL files
58+
__pycache__/
59+
*.py[cod]
60+
*$py.class
61+
62+
# C extensions
63+
*.so
64+
65+
# Distribution / packaging
66+
.Python
67+
build/
68+
develop-eggs/
69+
downloads/
70+
eggs/
71+
.eggs/
72+
lib/
73+
lib64/
74+
parts/
75+
sdist/
76+
var/
77+
wheels/
78+
pip-wheel-metadata/
79+
share/python-wheels/
80+
*.egg-info/
81+
.installed.cfg
82+
*.egg
83+
MANIFEST
84+
85+
# PyInstaller
86+
# Usually these files are written by a python script from a template
87+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
88+
*.manifest
89+
*.spec
90+
91+
# Installer logs
92+
pip-log.txt
93+
pip-delete-this-directory.txt
94+
95+
# Unit test / coverage reports
96+
htmlcov/
97+
.tox/
98+
.nox/
99+
.coverage
100+
.coverage.*
101+
.cache
102+
nosetests.xml
103+
coverage.xml
104+
*.cover
105+
.hypothesis/
106+
.pytest_cache/
107+
108+
# Translations
109+
*.mo
110+
*.pot
111+
112+
# Scrapy stuff:
113+
.scrapy
114+
115+
# Sphinx documentation
116+
docs/_build/
117+
118+
# PyBuilder
119+
target/
120+
121+
# pyenv
122+
.python-version
123+
124+
# pipenv
125+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
126+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
127+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
128+
# install all needed dependencies.
129+
#Pipfile.lock
130+
131+
# celery beat schedule file
132+
celerybeat-schedule
133+
134+
# SageMath parsed files
135+
*.sage.py
136+
137+
# Spyder project settings
138+
.spyderproject
139+
.spyproject
140+
141+
# Rope project settings
142+
.ropeproject
143+
144+
# Mr Developer
145+
.mr.developer.cfg
146+
.project
147+
.pydevproject
148+
149+
# mkdocs documentation
150+
/site
151+
152+
# mypy
153+
.mypy_cache/
154+
.dmypy.json
155+
dmypy.json
156+
157+
# Pyre type checker
158+
.pyre/
159+
160+
# End of https://www.gitignore.io/api/python,emacs

open-in-editor

+227
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
#!/usr/bin/env python3
2+
'''
3+
This scripts allows triggering opening emacs from a link on a webpage/browser extension via MIME.
4+
Handles links like:
5+
6+
editor:///path/tofile:123
7+
8+
See test_parse_uri for more examples.
9+
10+
To install (register the MIME handler), run
11+
12+
python3 mimemacs --editor emacs --install
13+
14+
You can use emacs/gvim as editors at the moment. If you want to add other editors, the code should be easy to follow.
15+
16+
You can check that it works with
17+
18+
xdg-open 'emacs:/path/to/some/file'
19+
20+
21+
I haven't found any existing mechanisms for this, please let me know if you know of any!
22+
23+
'''
24+
# TODO make it editor-agnostic? although supporting line numbers will be trickier
25+
26+
27+
# TODO not sure if it should be emacs:// or editor:?
28+
PROTOCOL = "emacs:"
29+
30+
31+
def test_parse_uri():
32+
assert parse_uri('emacs:/path/to/file') == (
33+
'/path/to/file',
34+
None,
35+
)
36+
37+
assert parse_uri('emacs:/path/with spaces') == (
38+
'/path/with spaces',
39+
None,
40+
)
41+
42+
assert parse_uri('emacs:/path/url%20encoded') == (
43+
'/path/url encoded',
44+
None,
45+
)
46+
47+
assert parse_uri('emacs:/path/to/file/and/line:10') == (
48+
'/path/to/file/and/line',
49+
10,
50+
)
51+
52+
import pytest # type: ignore
53+
with pytest.raises(Exception):
54+
parse_uri('badmime://whatever')
55+
56+
57+
def test_open_editor():
58+
from tempfile import TemporaryDirectory
59+
with TemporaryDirectory() as td:
60+
p = Path(td) / 'some file.org'
61+
p.write_text('''
62+
line 1
63+
line 2
64+
line 3 ---- THIS LINE SHOULD BE IN FOCUS!
65+
line 4
66+
'''.strip())
67+
open_editor(f'emacs:{p}:3', editor='emacs')
68+
69+
70+
import argparse
71+
from pathlib import Path
72+
import sys
73+
import subprocess
74+
from subprocess import check_call, run
75+
import tempfile
76+
from urllib.parse import unquote
77+
78+
79+
80+
def notify(what) -> None:
81+
# notify-send used as a user-facing means of error reporting
82+
run(["notify-send", what])
83+
84+
85+
def error(what) -> None:
86+
notify(what)
87+
raise RuntimeError(what)
88+
89+
90+
def install(editor: str) -> None:
91+
this_script = str(Path(__file__).absolute())
92+
CONTENT = f"""
93+
[Desktop Entry]
94+
Name=Emacs Mime handler
95+
Exec=python3 {this_script} --editor {editor} %u
96+
Icon=emacs-icon
97+
Type=Application
98+
Terminal=false
99+
MimeType=x-scheme-handler/emacs;
100+
""".strip()
101+
with tempfile.TemporaryDirectory() as td:
102+
pp = Path(td) / 'mimemacs.desktop'
103+
pp.write_text(CONTENT)
104+
check_call(['desktop-file-validate', str(pp)])
105+
check_call([
106+
'desktop-file-install',
107+
'--dir', str(Path('~/.local/share/applications').expanduser()),
108+
'--rebuild-mime-info-cache',
109+
str(pp),
110+
])
111+
112+
113+
from typing import Tuple, Optional, List
114+
Line = int
115+
File = str
116+
def parse_uri(uri: str) -> Tuple[File, Optional[Line]]:
117+
if not uri.startswith(PROTOCOL):
118+
error(f"Unexpected protocol {uri}")
119+
120+
uri = uri[len(PROTOCOL):]
121+
spl = uri.split(':')
122+
123+
linenum: Optional[int] = None
124+
if len(spl) == 1:
125+
pass # no lnum specified
126+
elif len(spl) == 2:
127+
uri = spl[0]
128+
# TODO could use that for column number? maybe an overkill though..
129+
# https://www.gnu.org/software/emacs/manual/html_node/emacs/emacsclient-Options.html
130+
linenum = int(spl[1])
131+
else:
132+
# TODO what if it actually has colons?
133+
error(f"Extra colons in URI {uri}")
134+
uri = unquote(uri)
135+
return (uri, linenum)
136+
137+
138+
def open_editor(uri: str, editor: str) -> None:
139+
uri, line = parse_uri(uri)
140+
141+
if editor == 'emacs':
142+
open_emacs(uri, line)
143+
elif editor == 'gvim':
144+
open_vim(uri, line)
145+
else:
146+
notify(f'Unexpected editor {editor}')
147+
import shutil
148+
for open_cmd in ['xdg-open', 'open']:
149+
if shutil.which(open_cmd):
150+
# sadly no generic way to handle line
151+
check_call([open_cmd, uri])
152+
break
153+
else:
154+
error('No xdg-open/open found!')
155+
156+
157+
def open_vim(uri: File, line: Optional[Line]) -> None:
158+
args = [uri] if line is None else [f'+{line}', uri]
159+
cmd = [
160+
'gvim',
161+
*args,
162+
]
163+
check_call(cmd)
164+
return
165+
166+
## alternatively, if you prefer a terminal vim
167+
cmd = [
168+
'vim',
169+
*args,
170+
]
171+
launch_in_terminal(cmd)
172+
173+
174+
175+
def open_emacs(uri: File, line: Optional[Line]) -> None:
176+
args = [uri] if line is None else [f'+{line}', uri]
177+
cmd = [
178+
'emacsclient',
179+
'--create-frame',
180+
# trick to run daemon if it isn't https://www.gnu.org/software/emacs/manual/html_node/emacs/emacsclient-Options.html
181+
'--alternate-editor=""',
182+
*args,
183+
]
184+
# todo exec?
185+
check_call(cmd)
186+
return
187+
188+
### alternatively, if you prefer a terminal emacs
189+
cmd = [
190+
'emacsclient',
191+
'--tty',
192+
'--alternate-editor=""',
193+
*args,
194+
]
195+
launch_in_terminal(cmd)
196+
###
197+
198+
def launch_in_terminal(cmd: List[str]):
199+
import shlex
200+
check_call([
201+
# NOTE: you might need xdg-terminal on some systems
202+
"x-terminal-emulator",
203+
"-e",
204+
' '.join(map(shlex.quote, cmd)),
205+
])
206+
207+
208+
209+
def main():
210+
p = argparse.ArgumentParser()
211+
p.add_argument('--editor', type=str, default='emacs', help='Editor to use (supported so far: emacs, gvim)')
212+
p.add_argument('--install', action='store_true', help='Pass to register MIME in your system')
213+
p.add_argument('uri', nargs='?')
214+
p.add_argument('--run-tests', action='store_true', help='Run unit tests')
215+
args = p.parse_args()
216+
if args.run_tests:
217+
# fuck, pytest can't run against a file without .py extension?
218+
test_parse_uri()
219+
test_open_editor()
220+
elif args.install:
221+
install(editor=args.editor)
222+
else:
223+
open_editor(args.uri, editor=args.editor)
224+
225+
226+
if __name__ == '__main__':
227+
main()

0 commit comments

Comments
 (0)