This commit is contained in:
Dima Gerasimov 2020-05-30 07:43:41 +01:00
commit 5dc42969b7
2 changed files with 387 additions and 0 deletions

160
.gitignore vendored Normal file
View file

@ -0,0 +1,160 @@
# Created by https://www.gitignore.io/api/python,emacs
# Edit at https://www.gitignore.io/?templates=python,emacs
### Emacs ###
# -*- mode: gitignore; -*-
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# Org-mode
.org-id-locations
*_archive
# flymake-mode
*_flymake.*
# eshell files
/eshell/history
/eshell/lastdir
# elpa packages
/elpa/
# reftex files
*.rel
# AUCTeX auto folder
/auto/
# cask packages
.cask/
dist/
# Flycheck
flycheck_*.el
# server auth directory
/server/
# projectiles files
.projectile
# directory configuration
.dir-locals.el
# network security
/network-security.data
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# Mr Developer
.mr.developer.cfg
.project
.pydevproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# End of https://www.gitignore.io/api/python,emacs

227
open-in-editor Executable file
View file

@ -0,0 +1,227 @@
#!/usr/bin/env python3
'''
This scripts allows triggering opening emacs from a link on a webpage/browser extension via MIME.
Handles links like:
editor:///path/tofile:123
See test_parse_uri for more examples.
To install (register the MIME handler), run
python3 mimemacs --editor emacs --install
You can use emacs/gvim as editors at the moment. If you want to add other editors, the code should be easy to follow.
You can check that it works with
xdg-open 'emacs:/path/to/some/file'
I haven't found any existing mechanisms for this, please let me know if you know of any!
'''
# TODO make it editor-agnostic? although supporting line numbers will be trickier
# TODO not sure if it should be emacs:// or editor:?
PROTOCOL = "emacs:"
def test_parse_uri():
assert parse_uri('emacs:/path/to/file') == (
'/path/to/file',
None,
)
assert parse_uri('emacs:/path/with spaces') == (
'/path/with spaces',
None,
)
assert parse_uri('emacs:/path/url%20encoded') == (
'/path/url encoded',
None,
)
assert parse_uri('emacs:/path/to/file/and/line:10') == (
'/path/to/file/and/line',
10,
)
import pytest # type: ignore
with pytest.raises(Exception):
parse_uri('badmime://whatever')
def test_open_editor():
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())
open_editor(f'emacs:{p}:3', editor='emacs')
import argparse
from pathlib import Path
import sys
import subprocess
from subprocess import check_call, run
import tempfile
from urllib.parse import unquote
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=Emacs Mime handler
Exec=python3 {this_script} --editor {editor} %u
Icon=emacs-icon
Type=Application
Terminal=false
MimeType=x-scheme-handler/emacs;
""".strip()
with tempfile.TemporaryDirectory() as td:
pp = Path(td) / 'mimemacs.desktop'
pp.write_text(CONTENT)
check_call(['desktop-file-validate', str(pp)])
check_call([
'desktop-file-install',
'--dir', str(Path('~/.local/share/applications').expanduser()),
'--rebuild-mime-info-cache',
str(pp),
])
from typing import Tuple, Optional, List
Line = int
File = str
def parse_uri(uri: str) -> Tuple[File, Optional[Line]]:
if not uri.startswith(PROTOCOL):
error(f"Unexpected protocol {uri}")
uri = uri[len(PROTOCOL):]
spl = uri.split(':')
linenum: Optional[int] = None
if len(spl) == 1:
pass # no lnum specified
elif len(spl) == 2:
uri = spl[0]
# TODO could use that for column number? maybe an overkill though..
# https://www.gnu.org/software/emacs/manual/html_node/emacs/emacsclient-Options.html
linenum = int(spl[1])
else:
# TODO what if it actually has colons?
error(f"Extra colons in URI {uri}")
uri = unquote(uri)
return (uri, linenum)
def open_editor(uri: str, editor: str) -> None:
uri, line = parse_uri(uri)
if editor == 'emacs':
open_emacs(uri, line)
elif editor == 'gvim':
open_vim(uri, line)
else:
notify(f'Unexpected editor {editor}')
import shutil
for open_cmd in ['xdg-open', 'open']:
if shutil.which(open_cmd):
# sadly no generic way to handle line
check_call([open_cmd, uri])
break
else:
error('No xdg-open/open found!')
def open_vim(uri: File, line: Optional[Line]) -> None:
args = [uri] if line is None else [f'+{line}', uri]
cmd = [
'gvim',
*args,
]
check_call(cmd)
return
## alternatively, if you prefer a terminal vim
cmd = [
'vim',
*args,
]
launch_in_terminal(cmd)
def open_emacs(uri: File, line: Optional[Line]) -> None:
args = [uri] if line is None else [f'+{line}', uri]
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
'--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)
###
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)),
])
def main():
p = argparse.ArgumentParser()
p.add_argument('--editor', type=str, default='emacs', help='Editor to use (supported so far: emacs, gvim)')
p.add_argument('--install', action='store_true', help='Pass to register MIME in your system')
p.add_argument('uri', nargs='?')
p.add_argument('--run-tests', action='store_true', help='Run unit tests')
args = p.parse_args()
if args.run_tests:
# fuck, pytest can't run against a file without .py extension?
test_parse_uri()
test_open_editor()
elif args.install:
install(editor=args.editor)
else:
open_editor(args.uri, editor=args.editor)
if __name__ == '__main__':
main()