From 5dc42969b7c53c4293b392216a067a356f9b22d4 Mon Sep 17 00:00:00 2001 From: Dima Gerasimov Date: Sat, 30 May 2020 07:43:41 +0100 Subject: [PATCH] initial: move script from https://github.com/karlicoss/dotemacs/blob/master/bin/mimemacs --- .gitignore | 160 ++++++++++++++++++++++++++++++++++ open-in-editor | 227 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 387 insertions(+) create mode 100644 .gitignore create mode 100755 open-in-editor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f13069 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/open-in-editor b/open-in-editor new file mode 100755 index 0000000..6ac5efa --- /dev/null +++ b/open-in-editor @@ -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()