├── .gitignore ├── README.org ├── browser ├── conkeror-clone-repo.js ├── edit-with-emacs.js ├── org-protocol-github-blob.user.js ├── org-protocol-github-comment.user.js ├── org-protocol-github-files.user.js └── org-protocol-github-repo.user.js ├── button.png ├── clone-button.png ├── org-protocol-github-lines.el ├── org-protocol-github-lines.png └── os └── xdg-open /.gitignore: -------------------------------------------------------------------------------- 1 | *.elc 2 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * org-protocol-github-lines 2 | 3 | The purpose of this plugin is to make it easier to switch from your 4 | browser to emacs when looking at code reviews 5 | [[file:http://puntoblogspot.blogspot.com.es/2012/10/github-emacs-org-protocol-github-lines.html][blogpost and demo]] 6 | 7 | [[./button.png]] 8 | ** Requirements 9 | - org-mode 10 | - org-protocol 11 | 12 | ** Manual and usage 13 | In github pull request views, add an emacs button that makes emacs open that file/line. 14 | [[file:http://puntoblogspot.blogspot.com.es/2012/10/github-emacs-org-protocol-github-lines.html][blogpost and demo]] 15 | 16 | At some point it got support for bitbucket repos also but it hasn't 17 | been updated in a while. PRs welcome. 18 | 19 | ** Code 20 | *** Firefox/Chrome 21 | Use tampermonkey or whatever other *monkey in your browser to add 22 | userscripts. 23 | 24 | Install [[./browser/edit-with-emacs.js]] to add the "Open with emacs" 25 | button on code comments. 26 | 27 | Install [[./browser/org-protocol-github-repo.user.js]] to add a "Clone" 28 | button on the main pages of repos. 29 | 30 | [[./clone-button.png]] 31 | 32 | *** OS 33 | os/xdg-open Predefine org-protocol to open emacsclient 34 | - https://orgmode.org/worg/org-contrib/org-protocol.html 35 | 36 | **** If you use Firefox: 37 | - http://kb.mozillazine.org/Register_protocol 38 | 39 | 40 | *** Emacs 41 | org-protocol-github-lines.el contains the code to open the actual file 42 | 43 | * Authors & contribs 44 | 45 | - [[http://github.com/kidd][Raimon Grau Cusco]] 46 | - [[https://github.com/ruediger][Rüdiger Sonderfeld]] 47 | -------------------------------------------------------------------------------- /browser/conkeror-clone-repo.js: -------------------------------------------------------------------------------- 1 | function clone_repo(I){ 2 | var document = I.buffer.document; 3 | var url = document.URL; 4 | var project = document.URL.match(/(bitbucket|github)\....\/([^/]+\/[^/]+)/); 5 | cloneLink = "org-protocol://"+ project[1] +"-clone://" + project[2]; 6 | document.location = cloneLink; 7 | } 8 | 9 | interactive("github-clone-repo", null, clone_repo ); 10 | interactive("clone-repo", null, clone_repo ); 11 | 12 | set_protocol_handler("org-protocol", find_file_in_path("emacsclient")); 13 | -------------------------------------------------------------------------------- /browser/edit-with-emacs.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name New Userscript 3 | // @namespace http://tampermonkey.net/ 4 | // @version 0.1 5 | // @description try to take over the world! 6 | // @author You 7 | // @match https://github.com/*/*/pull/* 8 | // @grant none 9 | // ==/UserScript== 10 | 11 | function createLink(fileName, lineNumber) { 12 | var project = document.URL.match(/github.com\/([^/]+\/[^/]+)/)[1]; 13 | return "org-protocol://github-lines://" + project + "/" + fileName + "/" + lineNumber; 14 | } 15 | 16 | (function() { 17 | 'use strict'; 18 | var forEach = Array.prototype.forEach; 19 | var comments=document.querySelectorAll(".js-inline-comments-container"); 20 | 21 | 22 | comments.forEach(function(f) { 23 | var file=f.previousElementSibling.previousElementSibling.firstElementChild.title; 24 | var a = f.previousElementSibling.querySelectorAll("td.blob-num"); 25 | var line = a[a.length-1].getAttribute("data-line-number"); 26 | 27 | 28 | //var buttonera = f.querySelector("form.js-resolvable-timeline-thread-form"); 29 | // var buttonera = f.querySelector("div.js-line-comments"); 30 | var buttonera = f; 31 | 32 | var bu=document.createElement("a"); 33 | bu.setAttribute("class", "btn m-3"); 34 | // bu.setAttribute("style", "float: left;"); 35 | bu.textContent = "Open in Emacs"; 36 | bu.setAttribute("href", createLink(file, line)); 37 | buttonera.appendChild(bu); 38 | 39 | 40 | var form= f.querySelector("form.js-resolvable-timeline-thread-form") 41 | form.setAttribute("style", "float: left;"); 42 | console.log(file , line); 43 | 44 | }); 45 | 46 | 47 | // Your code here... 48 | })(); 49 | -------------------------------------------------------------------------------- /browser/org-protocol-github-blob.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name org-protocol-github-blib 3 | // @description Insert Emacs links in blobs on github 4 | // @author Rüdiger Sonderfeld 5 | // @include https://github.com/*/blob/* 6 | // @version 0.1.0 7 | // @grant none 8 | // @license LGPL http://www.gnu.org/licenses/lgpl.html 9 | // ==/UserScript== 10 | 11 | // Emacs Icon is part of Emacs and GPLv3! 12 | var emacsIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAABGdBTUEAANbY1E9YMgAAA6JJREFUeNpiiQhZyfD%2FPyMDCEBpm%2F%2F%2FGYIZGRkcmBgZZf7%2B%2B8%2Fw%2B%2Fffx39%2B%2Fz%2Fw5%2B%2B%2FdRzsLEeAcgwsrIwMjEAGQACxMCAADyPD%2Fw6gohRmZgZ2Pj52Bi5uNgZRUS4Gfn4OEVZWZsMPH35mXb74evaXL78qgQZ8AWkCCCCYAXxAvP4%2Fwz8nKWluBg1NUQYODhaG37%2F%2BMTAxMTJwcLIwSEryMFhay7LfvfMhp7fzuOaXrz%2BDWFiYPwEEENgARsb%2F7UBXOTEAFfsHaDJYWcsx%2FPz5l4GRiYHh54%2B%2FDN%2B%2B%2FwLz%2F%2F1jZNDUEmWQleNxvnLlRwcLC0MWQACxAL0B8nMyIzMjAxcXK8PTp58Yfv3%2Bx%2FDl6y%2BGa1dfgoxnYAbKsbOzMHz%2B9Ivh%2Fr13DE%2BefGRgZ2NMAlq8DCCAgC5gjADaxA5yKkjR7dtvGU4cf8xgbCzF8ANo%2B%2F1770Ghy%2FD1y2%2BGt%2B%2B%2BMbx5843hL9ACZhZG9n%2F%2FGKIAAogFaIE1MLQZfv36y%2FD5808GLh5WhlMnHzP8%2BP6HwdBYgoEHGJDHjz1i%2BPjhB8OPn38YWFgYGf79BXr4H9jrFgABxAK0WAYYTQwqasIMxiZSDNLSfAw8PGwMf%2F4AvQG0Vd9IkoEbyN%2B86QbDR6AFIC%2BBESOYKQkQQCxAm%2F%2FZ2CowJKUag8IQ6Nr%2FQJf8Ynjw8D3DixdfGO4CvcQIlJCS4mN4%2FeoLAyhd%2FIcYA6QZ%2FwMEEMu%2Ff%2F8fKakIioEEfgNtvXnjDcPatVcZvgEDkYWFCZxY%2Fv79B5QDxgJQ538gAdbMCEpI%2Fx8DBBATKxvz4V077zDcvfsW7IJ3wID6%2BOE7MJCYwE4HJiIGISFOBn4%2BDgZOTlZgjDAhpT2GgwABxGygH%2F7x86ef0deuvmZlY2dmMDWTZjACxgAoHFiASRJsOzDU%2F4FsBtkKdPivX2D%2BJ6ABJQABxKynG%2FYEmKLEvn37bX7x4nOGVy%2B%2FMIiL8QBToxiDjrYYg7qGCIO8vAA4Nn4CY%2BHd2%2B%2FgGAP6oQNo3gqAAAIZAPLTUWBAmXNwsCoCwxAcnR%2BA0fb%2B%2FQ%2BwfzU0RBl09cQZlJWFGE4C08injz9XMjEzFgOl%2FgIEECwvfAaaFvD3z7%2FeHz9%2BJ719%2B40ZFNafPv1gePnyM8PlSy%2BBqfAHw7PnXz68ffNtAjBwW4D2%2FAVpBAgg5Nz4GWh7GtDWlW9efwsHRqc1UEwciP8AU9wjIH8fExPDCjZWlgvIoQgQYADfw28ecb4M7wAAAABJRU5ErkJggg%3D%3D"; 13 | 14 | var clipboardButton = document.querySelector('.zeroclipboard-button'); 15 | if(clipboardButton) { 16 | var path = clipboardButton.dataset.clipboardText; 17 | var project = document.URL.match(/github.com\/([^/]+\/[^/]+)/)[1]; 18 | var link = "org-protocol://github-lines:/" + project + '/' + path + "/"; 19 | var emacsLink = 'E'; 20 | 21 | // TODO: Could use some styling to make it look similar to the clipboard copy button. 22 | 23 | var range = document.createRange(); 24 | range.selectNode(clipboardButton.parentNode); 25 | var documentFragment = range.createContextualFragment(emacsLink); 26 | clipboardButton.parentNode.appendChild(documentFragment); 27 | } 28 | 29 | // Structure: 30 | -------------------------------------------------------------------------------- /browser/org-protocol-github-comment.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name org-protocol-github-comment 3 | // @description Insert emacs links to commented lines in gh pull requests. 4 | // @author Raimon Grau 5 | // @include https://github.com/*/pull/* 6 | // @include https://github.com/*/commit/* 7 | // @version 0.2.0 8 | // @grant none 9 | // @license LGPL http://www.gnu.org/licenses/lgpl.html 10 | // ==/UserScript== 11 | 12 | function createLink(fileName, lineNumber) { 13 | var project = document.URL.match(/github.com\/([^/]+\/[^/]+)/)[1]; 14 | return "org-protocol://github-lines://" + project + "/" + fileName + "/" + lineNumber; 15 | } 16 | 17 | (function() { 18 | 'use strict'; 19 | var forEach = Array.prototype.forEach; 20 | var comments=document.querySelectorAll(".js-inline-comments-container"); 21 | 22 | 23 | comments.forEach(function(f) { 24 | var file=f.previousElementSibling.previousElementSibling.firstElementChild.title; 25 | var a = f.previousElementSibling.querySelectorAll("td.js-linkable-line-number"); 26 | var line = a[a.length-1].getAttribute("data-line-number"); 27 | 28 | 29 | var buttonera = f.querySelector("form.js-resolvable-timeline-thread-form"); 30 | var bu=document.createElement("button"); 31 | bu.setAttribute("class", "btn m-3"); 32 | bu.textContent = "Open in editorrr"; 33 | bu.setAttribute("href", createLink(file, line)); 34 | buttonera.appendChild(bu); 35 | console.log(file , line); 36 | 37 | }); 38 | 39 | 40 | // Your code here... 41 | })(); 42 | -------------------------------------------------------------------------------- /browser/org-protocol-github-files.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name org-protocol-github-files 3 | // @description Insert Emacs links for files on github 4 | // @author Rüdiger Sonderfeld 5 | // @include https://github.com/* 6 | // @version 0.1.0 7 | // @grant none 8 | // @license LGPL http://www.gnu.org/licenses/lgpl.html 9 | // ==/UserScript== 10 | 11 | var forEach = Array.prototype.forEach; 12 | 13 | // Emacs Icon is part of Emacs and GPLv3! 14 | var emacsIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8%2F9hAAAABGdBTUEAANbY1E9YMgAAA6JJREFUeNpiiQhZyfD%2FPyMDCEBpm%2F%2F%2FGYIZGRkcmBgZZf7%2B%2B8%2Fw%2B%2Fffx39%2B%2Fz%2Fw5%2B%2B%2FdRzsLEeAcgwsrIwMjEAGQACxMCAADyPD%2Fw6gohRmZgZ2Pj52Bi5uNgZRUS4Gfn4OEVZWZsMPH35mXb74evaXL78qgQZ8AWkCCCCYAXxAvP4%2Fwz8nKWluBg1NUQYODhaG37%2F%2BMTAxMTJwcLIwSEryMFhay7LfvfMhp7fzuOaXrz%2BDWFiYPwEEENgARsb%2F7UBXOTEAFfsHaDJYWcsx%2FPz5l4GRiYHh54%2B%2FDN%2B%2B%2FwLz%2F%2F1jZNDUEmWQleNxvnLlRwcLC0MWQACxAL0B8nMyIzMjAxcXK8PTp58Yfv3%2Bx%2FDl6y%2BGa1dfgoxnYAbKsbOzMHz%2B9Ivh%2Fr13DE%2BefGRgZ2NMAlq8DCCAgC5gjADaxA5yKkjR7dtvGU4cf8xgbCzF8ANo%2B%2F1770Ghy%2FD1y2%2BGt%2B%2B%2BMbx5843hL9ACZhZG9n%2F%2FGKIAAogFaIE1MLQZfv36y%2FD5808GLh5WhlMnHzP8%2BP6HwdBYgoEHGJDHjz1i%2BPjhB8OPn38YWFgYGf79BXr4H9jrFgABxAK0WAYYTQwqasIMxiZSDNLSfAw8PGwMf%2F4AvQG0Vd9IkoEbyN%2B86QbDR6AFIC%2BBESOYKQkQQCxAm%2F%2FZ2CowJKUag8IQ6Nr%2FQJf8Ynjw8D3DixdfGO4CvcQIlJCS4mN4%2FeoLAyhd%2FIcYA6QZ%2FwMEEMu%2Ff%2F8fKakIioEEfgNtvXnjDcPatVcZvgEDkYWFCZxY%2Fv79B5QDxgJQ538gAdbMCEpI%2Fx8DBBATKxvz4V077zDcvfsW7IJ3wID6%2BOE7MJCYwE4HJiIGISFOBn4%2BDgZOTlZgjDAhpT2GgwABxGygH%2F7x86ef0deuvmZlY2dmMDWTZjACxgAoHFiASRJsOzDU%2F4FsBtkKdPivX2D%2BJ6ABJQABxKynG%2FYEmKLEvn37bX7x4nOGVy%2B%2FMIiL8QBToxiDjrYYg7qGCIO8vAA4Nn4CY%2BHd2%2B%2FgGAP6oQNo3gqAAAIZAPLTUWBAmXNwsCoCwxAcnR%2BA0fb%2B%2FQ%2BwfzU0RBl09cQZlJWFGE4C08injz9XMjEzFgOl%2FgIEECwvfAaaFvD3z7%2FeHz9%2BJ719%2B40ZFNafPv1gePnyM8PlSy%2BBqfAHw7PnXz68ffNtAjBwW4D2%2FAVpBAgg5Nz4GWh7GtDWlW9efwsHRqc1UEwciP8AU9wjIH8fExPDCjZWlgvIoQgQYADfw28ecb4M7wAAAABJRU5ErkJggg%3D%3D"; 15 | 16 | /* 17 | * Structure is 18 | * 19 | */ 20 | 21 | var textFiles = document.querySelectorAll(".mini-icon-text-file"); 22 | forEach.call(textFiles, function(textFile) { 23 | var tr = textFile.parentNode; 24 | if(!tr) { 25 | return; 26 | } 27 | var content = tr.nextElementSibling; 28 | if(!content) { 29 | return; 30 | } 31 | var fileLink = content.firstElementChild; 32 | if(!fileLink) { 33 | return; 34 | } 35 | 36 | var href = fileLink.getAttribute('href'); 37 | if(!href) { 38 | return; 39 | } 40 | 41 | var file = href.replace(/(blob|tree)\/[^\/]+\//, ""); // remove /blob/branch/ part 42 | var orgLink = "org-protocol://github-lines:/" + file + "/"; 43 | var imgHTML = 'E'; 44 | 45 | var range = document.createRange(); 46 | range.selectNode(tr); 47 | var documentFragment = range.createContextualFragment(imgHTML); 48 | tr.replaceChild(documentFragment, textFile); 49 | }); 50 | -------------------------------------------------------------------------------- /browser/org-protocol-github-repo.user.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name org-protocol-github-repo 3 | // @description Insert Emacs links for files on github 4 | // @author Raimon Grau Cusco 5 | // @include https://github.com/* 6 | // @version 0.1.0 7 | // @grant none 8 | // @license LGPL http://www.gnu.org/licenses/lgpl.html 9 | // ==/UserScript== 10 | 11 | 12 | var insertAfter = function(e,i){ 13 | if(e.nextSibling){ 14 | e.parentNode.insertBefore(i,e.nextSibling); 15 | } else { 16 | e.parentNode.appendChild(i); 17 | } 18 | }; 19 | 20 | var url = document.URL; 21 | var project = document.URL.match(/github.com\/([^/]+\/[^/]+)/)[1]; 22 | var repoTitle = document.querySelectorAll("get-repo"); 23 | 24 | var mydiv = document.createElement("div"); 25 | mydiv.classList = "d-none d-md-flex ml-2"; 26 | var cloneLink = document.createElement("a"); 27 | cloneLink.classList = "btn btn-primary"; 28 | 29 | cloneLink.appendChild(document.createTextNode("Clone")); 30 | 31 | cloneLink.href = "org-protocol://github-clone://" + project; 32 | mydiv.appendChild(cloneLink); 33 | var title = repoTitle[0].parentNode; 34 | insertAfter(title, mydiv); 35 | -------------------------------------------------------------------------------- /button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidd/org-protocol-github-lines/f5a0b9d727f103c9e371483604365051c8cac548/button.png -------------------------------------------------------------------------------- /clone-button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidd/org-protocol-github-lines/f5a0b9d727f103c9e371483604365051c8cac548/clone-button.png -------------------------------------------------------------------------------- /org-protocol-github-lines.el: -------------------------------------------------------------------------------- 1 | ;;; org-protocol-github-lines.el --- Open files and lines in emacs from your browser (when on github) 2 | 3 | ;; Copyright (C) 2012 Raimon Grau 4 | 5 | ;; Author: Raimon Grau 6 | ;; Keywords: tools, extensions 7 | 8 | ;; This program is free software; you can redistribute it and/or modify 9 | ;; it under the terms of the GNU General Public License as published by 10 | ;; the Free Software Foundation, either version 3 of the License, or 11 | ;; (at your option) any later version. 12 | 13 | ;; This program is distributed in the hope that it will be useful, 14 | ;; but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | ;; GNU General Public License for more details. 17 | 18 | ;; You should have received a copy of the GNU General Public License 19 | ;; along with this program. If not, see . 20 | 21 | ;;; Commentary: 22 | 23 | ;; 24 | 25 | ;;; Code: 26 | (require 'org-protocol) 27 | (require 'cl-macs) 28 | 29 | (defgroup org-protocol-github nil 30 | "Browser to Emacs interface for GitHub" 31 | :prefix "org-protocol-github-" 32 | :group 'tools) 33 | 34 | (defcustom org-protocol-github-projects nil 35 | "Map of GitHub projects to directories. 36 | See also `org-protocol-github-project-directories'." 37 | :group 'org-protocol-github 38 | :type '(repeat (cons (string :tag "GitHub project name (user/project)") 39 | (directory :tag "Project directory")))) 40 | 41 | (defcustom org-protocol-github-project-directories (list temporary-file-directory) 42 | "List of directories where projects are stored. 43 | See also `org-protocol-github-projects'." 44 | :group 'org-protocol-github 45 | :type '(repeat directory)) 46 | 47 | (defun org-protocol-github--find-project-directory (user project) 48 | "Find the github project specified by USER and PROJECT. 49 | If there is no mapping in `org-protocol-github-projects' then 50 | `org-protocol-github-project-directories' is searched for a directory named 51 | after the project." 52 | (let ((key (concat user "/" project))) 53 | (or 54 | (cdr (assoc key org-protocol-github-projects)) 55 | (cl-loop for d in org-protocol-github-project-directories 56 | for file-name = (expand-file-name project 57 | (file-name-as-directory d)) 58 | when (file-exists-p file-name) 59 | return file-name)))) 60 | 61 | ;;;###autoload 62 | (defun org-protocol-github-lines (data) 63 | "Handle github-lines protocol. 64 | DATA contains the user/project/file/line information." 65 | (let* ((content (org-protocol-split-data data t)) 66 | (user (car content)) 67 | (project (cadr content)) 68 | (file (butlast (cddr content))) 69 | (line (car (last content))) 70 | (dir (org-protocol-github--find-project-directory user 71 | project))) 72 | (message "%s" content) 73 | (unless dir 74 | (error "Project %s/%s not found on local machine." user project)) 75 | (with-current-buffer 76 | (find-file 77 | (mapconcat 'identity (cons dir file) "/")) 78 | (when line 79 | (goto-char (point-min)) 80 | (forward-line (1- (string-to-number line)))))) 81 | nil) 82 | 83 | (defun org-protocol-clone-repo (data server) 84 | (let* ((content (org-protocol-split-data data t)) 85 | (user (car content)) 86 | (project (cadr content)) 87 | (dir (org-protocol-github--find-project-directory user 88 | project))) 89 | (if dir 90 | (message "repo %s/%s is already in" user project) 91 | (org-protocol--clone user project server)) 92 | (format "%s%s/" (car org-protocol-github-project-directories) project))) 93 | 94 | (defun org-protocol--clone (user project server) 95 | (let ((default-directory (car org-protocol-github-project-directories))) 96 | (async-shell-command (format "git clone git@%s:%s/%s.git" server user project)))) 97 | 98 | (defun org-protocol-github-repo (data) 99 | (org-protocol-clone-repo data "github.com")) 100 | 101 | (defun org-protocol-bitbucket-repo (data) 102 | (org-protocol-clone-repo data "bitbucket.org")) 103 | 104 | ;;;###autoload 105 | (add-to-list 'org-protocol-protocol-alist 106 | '("Open files from GitHub." 107 | :protocol "github-lines" 108 | :function org-protocol-github-lines)) 109 | 110 | ;;;###autoload 111 | (add-to-list 'org-protocol-protocol-alist 112 | '("Clone repos from GitHub." 113 | :protocol "github-clone" 114 | :function org-protocol-github-repo)) 115 | 116 | ;;;###autoload 117 | (add-to-list 'org-protocol-protocol-alist 118 | '("Clone repos from Bitbucket." 119 | :protocol "bitbucket-clone" 120 | :function org-protocol-bitbucket-repo)) 121 | 122 | (provide 'org-protocol-github-lines) 123 | ;;; org-protocol-github-lines.el ends here 124 | -------------------------------------------------------------------------------- /org-protocol-github-lines.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kidd/org-protocol-github-lines/f5a0b9d727f103c9e371483604365051c8cac548/org-protocol-github-lines.png -------------------------------------------------------------------------------- /os/xdg-open: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #--------------------------------------------- 3 | # xdg-open 4 | # 5 | # Utility script to open a URL in the registered default application. 6 | # 7 | # Refer to the usage() function below for usage. 8 | # 9 | # Copyright 2006, Kevin Krammer 10 | # Copyright 2006, Jeremy White 11 | # 12 | # LICENSE: 13 | # 14 | # Permission is hereby granted, free of charge, to any person obtaining a 15 | # copy of this software and associated documentation files (the "Software"), 16 | # to deal in the Software without restriction, including without limitation 17 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 18 | # and/or sell copies of the Software, and to permit persons to whom the 19 | # Software is furnished to do so, subject to the following conditions: 20 | # 21 | # The above copyright notice and this permission notice shall be included 22 | # in all copies or substantial portions of the Software. 23 | # 24 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 25 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 26 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 27 | # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 28 | # OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 29 | # ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 30 | # OTHER DEALINGS IN THE SOFTWARE. 31 | # 32 | #--------------------------------------------- 33 | 34 | manualpage() 35 | { 36 | cat << _MANUALPAGE 37 | Name 38 | 39 | xdg-open - opens a file or URL in the user's preferred application 40 | 41 | Synopsis 42 | 43 | xdg-open { file | URL } 44 | 45 | xdg-open { --help | --manual | --version } 46 | 47 | Description 48 | 49 | xdg-open opens a file or URL in the user's preferred application. If a URL is 50 | provided the URL will be opened in the user's preferred web browser. If a file 51 | is provided the file will be opened in the preferred application for files of 52 | that type. xdg-open supports file, ftp, http and https URLs. 53 | 54 | xdg-open is for use inside a desktop session only. It is not recommended to use 55 | xdg-open as root. 56 | 57 | Options 58 | 59 | --help 60 | Show command synopsis. 61 | --manual 62 | Show this manualpage. 63 | --version 64 | Show the xdg-utils version information. 65 | 66 | Exit Codes 67 | 68 | An exit code of 0 indicates success while a non-zero exit code indicates 69 | failure. The following failure codes can be returned: 70 | 71 | 1 72 | Error in command line syntax. 73 | 2 74 | One of the files passed on the command line did not exist. 75 | 3 76 | A required tool could not be found. 77 | 4 78 | The action failed. 79 | 80 | Examples 81 | 82 | xdg-open 'http://www.freedesktop.org/' 83 | 84 | Opens the Freedesktop.org website in the user's default browser 85 | 86 | xdg-open /tmp/foobar.png 87 | 88 | Opens the PNG image file /tmp/foobar.png in the user's default image viewing 89 | application. 90 | 91 | _MANUALPAGE 92 | } 93 | 94 | usage() 95 | { 96 | cat << _USAGE 97 | xdg-open - opens a file or URL in the user's preferred application 98 | 99 | Synopsis 100 | 101 | xdg-open { file | URL } 102 | 103 | xdg-open { --help | --manual | --version } 104 | 105 | _USAGE 106 | } 107 | 108 | #@xdg-utils-common@ 109 | 110 | #---------------------------------------------------------------------------- 111 | # Common utility functions included in all XDG wrapper scripts 112 | #---------------------------------------------------------------------------- 113 | 114 | DEBUG() 115 | { 116 | [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && return 0; 117 | [ ${XDG_UTILS_DEBUG_LEVEL} -lt $1 ] && return 0; 118 | shift 119 | echo "$@" >&2 120 | } 121 | 122 | #------------------------------------------------------------- 123 | # Exit script on successfully completing the desired operation 124 | 125 | exit_success() 126 | { 127 | if [ $# -gt 0 ]; then 128 | echo "$@" 129 | echo 130 | fi 131 | 132 | exit 0 133 | } 134 | 135 | 136 | #----------------------------------------- 137 | # Exit script on malformed arguments, not enough arguments 138 | # or missing required option. 139 | # prints usage information 140 | 141 | exit_failure_syntax() 142 | { 143 | if [ $# -gt 0 ]; then 144 | echo "xdg-open: $@" >&2 145 | echo "Try 'xdg-open --help' for more information." >&2 146 | else 147 | usage 148 | echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info." 149 | fi 150 | 151 | exit 1 152 | } 153 | 154 | #------------------------------------------------------------- 155 | # Exit script on missing file specified on command line 156 | 157 | exit_failure_file_missing() 158 | { 159 | if [ $# -gt 0 ]; then 160 | echo "xdg-open: $@" >&2 161 | fi 162 | 163 | exit 2 164 | } 165 | 166 | #------------------------------------------------------------- 167 | # Exit script on failure to locate necessary tool applications 168 | 169 | exit_failure_operation_impossible() 170 | { 171 | if [ $# -gt 0 ]; then 172 | echo "xdg-open: $@" >&2 173 | fi 174 | 175 | exit 3 176 | } 177 | 178 | #------------------------------------------------------------- 179 | # Exit script on failure returned by a tool application 180 | 181 | exit_failure_operation_failed() 182 | { 183 | if [ $# -gt 0 ]; then 184 | echo "xdg-open: $@" >&2 185 | fi 186 | 187 | exit 4 188 | } 189 | 190 | #------------------------------------------------------------ 191 | # Exit script on insufficient permission to read a specified file 192 | 193 | exit_failure_file_permission_read() 194 | { 195 | if [ $# -gt 0 ]; then 196 | echo "xdg-open: $@" >&2 197 | fi 198 | 199 | exit 5 200 | } 201 | 202 | #------------------------------------------------------------ 203 | # Exit script on insufficient permission to read a specified file 204 | 205 | exit_failure_file_permission_write() 206 | { 207 | if [ $# -gt 0 ]; then 208 | echo "xdg-open: $@" >&2 209 | fi 210 | 211 | exit 6 212 | } 213 | 214 | check_input_file() 215 | { 216 | if [ ! -e "$1" ]; then 217 | exit_failure_file_missing "file '$1' does not exist" 218 | fi 219 | if [ ! -r "$1" ]; then 220 | exit_failure_file_permission_read "no permission to read file '$1'" 221 | fi 222 | } 223 | 224 | check_vendor_prefix() 225 | { 226 | file_label="$2" 227 | [ -n "$file_label" ] || file_label="filename" 228 | file=`basename "$1"` 229 | case "$file" in 230 | [a-zA-Z]*-*) 231 | return 232 | ;; 233 | esac 234 | 235 | echo "xdg-open: $file_label '$file' does not have a proper vendor prefix" >&2 236 | echo 'A vendor prefix consists of alpha characters ([a-zA-Z]) and is terminated' >&2 237 | echo 'with a dash ("-"). An example '"$file_label"' is '"'example-$file'" >&2 238 | echo "Use --novendor to override or 'xdg-open --manual' for additional info." >&2 239 | exit 1 240 | } 241 | 242 | check_output_file() 243 | { 244 | # if the file exists, check if it is writeable 245 | # if it does not exists, check if we are allowed to write on the directory 246 | if [ -e "$1" ]; then 247 | if [ ! -w "$1" ]; then 248 | exit_failure_file_permission_write "no permission to write to file '$1'" 249 | fi 250 | else 251 | DIR=`dirname "$1"` 252 | if [ ! -w "$DIR" -o ! -x "$DIR" ]; then 253 | exit_failure_file_permission_write "no permission to create file '$1'" 254 | fi 255 | fi 256 | } 257 | 258 | #---------------------------------------- 259 | # Checks for shared commands, e.g. --help 260 | 261 | check_common_commands() 262 | { 263 | while [ $# -gt 0 ] ; do 264 | parm="$1" 265 | shift 266 | 267 | case "$parm" in 268 | --help) 269 | usage 270 | echo "Use 'man xdg-open' or 'xdg-open --manual' for additional info." 271 | exit_success 272 | ;; 273 | 274 | --manual) 275 | manualpage 276 | exit_success 277 | ;; 278 | 279 | --version) 280 | echo "xdg-open 1.0.1" 281 | exit_success 282 | ;; 283 | esac 284 | done 285 | } 286 | 287 | check_common_commands "$@" 288 | 289 | [ -z "${XDG_UTILS_DEBUG_LEVEL}" ] && unset XDG_UTILS_DEBUG_LEVEL; 290 | if [ ${XDG_UTILS_DEBUG_LEVEL-0} -lt 1 ]; then 291 | # Be silent 292 | xdg_redirect_output=" > /dev/null 2> /dev/null" 293 | else 294 | # All output to stderr 295 | xdg_redirect_output=" >&2" 296 | fi 297 | 298 | #-------------------------------------- 299 | # Checks for known desktop environments 300 | # set variable DE to the desktop environments name, lowercase 301 | 302 | detectDE() 303 | { 304 | if [ x"$KDE_FULL_SESSION" = x"true" ]; then DE=kde; 305 | elif [ x"$GNOME_DESKTOP_SESSION_ID" != x"" ]; then DE=gnome; 306 | elif xprop -root _DT_SAVE_MODE | grep ' = \"xfce4\"$' >/dev/null 2>&1; then DE=xfce; 307 | fi 308 | } 309 | 310 | #---------------------------------------------------------------------------- 311 | # kfmclient exec/openURL can give bogus exit value in KDE <= 3.5.4 312 | # It also always returns 1 in KDE 3.4 and earlier 313 | # Simply return 0 in such case 314 | 315 | kfmclient_fix_exit_code() 316 | { 317 | version=`kde-config --version 2>/dev/null | grep KDE` 318 | major=`echo $version | sed 's/KDE: \([0-9]\).*/\1/'` 319 | minor=`echo $version | sed 's/KDE: [0-9]*\.\([0-9]\).*/\1/'` 320 | release=`echo $version | sed 's/KDE: [0-9]*\.[0-9]*\.\([0-9]\).*/\1/'` 321 | test "$major" -gt 3 && return $1 322 | test "$minor" -gt 5 && return $1 323 | test "$release" -gt 4 && return $1 324 | return 0 325 | } 326 | 327 | open_kde() 328 | { 329 | which kfmclient >/dev/null || open_generic "$1" 330 | 331 | kfmclient exec "$1" 332 | kfmclient_fix_exit_code $? 333 | 334 | if [ $? -eq 0 ]; then 335 | exit_success 336 | else 337 | exit_failure_operation_failed 338 | fi 339 | } 340 | 341 | open_gnome() 342 | { 343 | gnome-open "$1" 344 | 345 | if [ $? -eq 0 ]; then 346 | exit_success 347 | else 348 | exit_failure_operation_failed 349 | fi 350 | } 351 | 352 | open_xfce() 353 | { 354 | exo-open "$1" 355 | 356 | if [ $? -eq 0 ]; then 357 | exit_success 358 | else 359 | exit_failure_operation_failed 360 | fi 361 | } 362 | 363 | open_generic() 364 | { 365 | # If it's a path or a file:/// URL, we can open it with run-mailcap 366 | if which run-mailcap >/dev/null && 367 | (echo "$1" | grep -q '^file:///' || 368 | ! echo "$1" | egrep -q '^[a-zA-Z+\.\-]+:'); then 369 | 370 | local file 371 | file="$1" 372 | 373 | # Decode URLs 374 | if echo "$file" | grep -q '^file:///'; then 375 | file=${file#file://} 376 | file=$(echo "$file" | perl -pe 's/%(..)/pack("c", hex($1))/eg') 377 | fi 378 | 379 | run-mailcap --action=view "$file" 380 | 381 | if [ $? -eq 0 ]; then 382 | exit_success 383 | else 384 | exit_failure_operation_failed 385 | fi 386 | 387 | elif (echo "$1" | grep -q '^org-protocol:'); then 388 | emacsclient -n "$1" 389 | if [ $? -eq 0 ]; then 390 | exit_success 391 | fi 392 | sensible-browser "$1" 393 | 394 | if [ $? -eq 0 ]; then 395 | exit_success 396 | else 397 | exit_failure_operation_failed 398 | fi 399 | fi 400 | } 401 | 402 | [ x"$1" != x"" ] || exit_failure_syntax 403 | 404 | url= 405 | while [ $# -gt 0 ] ; do 406 | parm="$1" 407 | shift 408 | 409 | case "$parm" in 410 | -*) 411 | exit_failure_syntax "unexpected option '$parm'" 412 | ;; 413 | 414 | *) 415 | if [ -n "$url" ] ; then 416 | exit_failure_syntax "unexpected argument '$parm'" 417 | fi 418 | url="$parm" 419 | ;; 420 | esac 421 | done 422 | 423 | if [ -z "${url}" ] ; then 424 | exit_failure_syntax "file or URL argument missing" 425 | fi 426 | 427 | detectDE 428 | 429 | if [ x"$DE" = x"" ]; then 430 | DE=generic 431 | fi 432 | 433 | case "$DE" in 434 | kde) 435 | open_kde "$url" 436 | ;; 437 | 438 | gnome) 439 | open_gnome "$url" 440 | ;; 441 | 442 | xfce) 443 | open_xfce "$url" 444 | ;; 445 | 446 | generic) 447 | open_generic "$url" 448 | ;; 449 | 450 | *) 451 | exit_failure_operation_impossible "no method available for opening '$url'" 452 | ;; 453 | esac 454 | --------------------------------------------------------------------------------