├── README.md └── gist.el /README.md: -------------------------------------------------------------------------------- 1 | gist.el -- Emacs integration for gist.github.com 2 | ================================================ 3 | 4 | Uses your local GitHub config if it can find it. 5 | 6 | Go to your [GitHub Settings](https://github.com/settings/tokens) and generate 7 | a personal access token with at least `gist` scope. If you intend to use more 8 | of the underlying `gh.el` library, it's recommended you also add the `user` and 9 | `repo` scopes. 10 | 11 | Next run: 12 | 13 | ``` Shell 14 | git config --global github.user 15 | git config --global github.oauth-token 16 | ``` 17 | 18 | News 19 | ==== 20 | 21 | What's new in 1.4.0 ? 22 | --------------------- 23 | 24 | * support #tags in gist description 25 | * support limiting display by tags, visibility 26 | 27 | What's new in 1.3.0 ? 28 | --------------------- 29 | 30 | * support listing another user's gists 31 | * more keybindings for (un)starring, forking gists 32 | * optionally ask for description at gist creation time 33 | 34 | What's new in 1.2.0 ? 35 | --------------------- 36 | 37 | * make gist list appearance customizable 38 | * more robust mode detection 39 | * add ability to open gist without changing focus 40 | * add ability to open current gist in browser 41 | 42 | What's new in 1.1.0 ? 43 | --------------------- 44 | 45 | * support for multiple profiles (e.g. github.com and Github Enterprise instance) 46 | * remove calls to deprecated gh.el APIs 47 | * support for background-reloading of gist list 48 | 49 | What's new in 1.0 ? 50 | ------------------- 51 | 52 | * gist.el now maintains a local cache so as to not go to the gist server every now and then. 53 | * multi-files gist support (indicated by a '+' in the gist list) 54 | * improved gist-list buffer, based on tabulated-list.el (same codebase as package.el) 55 | New keybindings: 56 | * `g` : reload the gist list from server 57 | * `e` : edit current gist description 58 | * `k` : delete current gist 59 | * `+` : add a file to the current gist 60 | * `-` : remove a file from the current gist 61 | * `y` : print current gist url 62 | * `b` : browse current gist 63 | * `*` : star gist 64 | * `^` : unstar gist 65 | * `f` : fork gist 66 | * in-place edition. While viewing a gist file buffer, you can: 67 | * `C-x C-s` : save a new version of the gist 68 | * `C-x C-w` : rename some file 69 | * dired integration. From a dired buffer, you can: 70 | * `@` : make a gist out of marked files (with a prefix, make it private) 71 | 72 | Install 73 | ======= 74 | 75 | Dependencies 76 | ------------ 77 | 78 | gist.el depends on a number of other modules, that you'll need to install, either manually or by way of Emacs package manager 79 | 80 | * tabulated-list.el 81 | built-in for emacs 24. If you're using emacs 23, you can find a backport here: https://github.com/sigma/tabulated-list.el 82 | * gh.el 83 | GitHub client library. Install from there: https://github.com/sigma/gh.el 84 | * pcache.el 85 | Really a gh.el dependency. Install from there: https://github.com/sigma/pcache 86 | * logito.el 87 | Really a gh.el dependency. Install from there: https://github.com/sigma/logito 88 | 89 | Install gist.el from marmalade (recommended) 90 | -------------------------------------------- 91 | 92 | In that scenario, you don't have to deal with the above dependencies yourself. 93 | 94 | For emacs 24, first make sure http://marmalade-repo.org/ is properly configured. Then 95 | 96 | M-x package-install RET gist RET 97 | 98 | For emacs 23, you'll need to install a version of package.el first. Some bootstrap code is available there: https://gist.github.com/1884169 99 | Then proceed as for emacs 24. You might get some compilation errors, but the package should be operational in the end. 100 | 101 | Install gist.el from git 102 | ------------------------ 103 | 104 | After installing the required dependencies, proceed with: 105 | 106 | $ cd ~/.emacs.d/vendor 107 | $ git clone git://github.com/defunkt/gist.el.git 108 | 109 | In your emacs config: 110 | 111 | (add-to-list 'load-path "~/.emacs.d/vendor/gist.el") 112 | (require 'gist) 113 | 114 | Getting started 115 | =============== 116 | 117 | When you first run a gist.el operation, you might be asked for your GitHub username and password. The username will be stored for future use, and a OAuth token will be stored in place of your password. 118 | 119 | To make gist.el forget about those information, just remove them from your ~/.gitconfig file 120 | 121 | Functions 122 | ========= 123 | 124 | gist-list - Lists your gists in a new buffer. Use arrow keys 125 | to browse, RET to open one in the other buffer. 126 | 127 | gist-region - Copies Gist URL into the kill ring. 128 | With a prefix argument, makes a private gist. 129 | 130 | gist-region-private - Explicitly create a private gist. 131 | 132 | gist-buffer - Copies Gist URL into the kill ring. 133 | With a prefix argument, makes a private gist. 134 | 135 | gist-buffer-private - Explicitly create a private gist. 136 | 137 | gist-region-or-buffer - Post either the current region, or if mark 138 | is not set, the current buffer as a new paste at gist.github.com . 139 | Copies the URL into the kill ring. 140 | With a prefix argument, makes a private paste. 141 | 142 | gist-region-or-buffer-private - Explicitly create a gist from the 143 | region or buffer. 144 | 145 | Config 146 | ====== 147 | 148 | Set `gist-view-gist` to non-nil if you want to view your Gist using 149 | `browse-url` after it is created. 150 | 151 | Meta 152 | ==== 153 | 154 | * Code: `git clone git://github.com/defunkt/gist.el.git` 155 | * Home: 156 | * Bugs: 157 | -------------------------------------------------------------------------------- /gist.el: -------------------------------------------------------------------------------- 1 | ;;; gist.el --- Emacs integration for gist.github.com 2 | 3 | ;; Author: Yann Hodique 4 | ;; Original Author: Christian Neukirchen 5 | ;; Contributors: Chris Wanstrath 6 | ;; Will Farrington 7 | ;; Michael Ivey 8 | ;; Phil Hagelberg 9 | ;; Dan McKinley 10 | ;; Marcelo Muñoz Araya 11 | ;; Version: 1.4.0 12 | ;; Package-Requires: ((emacs "24.1") (gh "0.10.0")) 13 | ;; Keywords: tools 14 | ;; Homepage: https://github.com/defunkt/gist.el 15 | 16 | ;; This file is NOT part of GNU Emacs. 17 | 18 | ;; This is free software; you can redistribute it and/or modify it under 19 | ;; the terms of the GNU General Public License as published by the Free 20 | ;; Software Foundation; either version 2, or (at your option) any later 21 | ;; version. 22 | ;; 23 | ;; This is distributed in the hope that it will be useful, but WITHOUT 24 | ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 25 | ;; FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 26 | ;; for more details. 27 | ;; 28 | ;; You should have received a copy of the GNU General Public License 29 | ;; along with GNU Emacs; see the file COPYING. If not, write to the 30 | ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, 31 | ;; MA 02111-1307, USA. 32 | 33 | ;;; Commentary: 34 | 35 | ;; An Emacs interface for managing gists (http://gist.github.com). 36 | 37 | ;;; Code: 38 | 39 | (eval-when-compile 40 | (require 'cl)) 41 | 42 | (require 'eieio) 43 | (require 'eieio-base) 44 | (require 'timezone) 45 | 46 | (require 'gh-api) 47 | (require 'gh-gist) 48 | (require 'gh-profile) 49 | 50 | (require 'tabulated-list) 51 | 52 | (defgroup gist nil 53 | "Interface to GitHub's Gist." 54 | :group 'applications) 55 | 56 | (defcustom gist-list-format '((id "Id" 10 nil identity) 57 | (created "Created" 20 nil "%D %R") 58 | (visibility "Visibility" 10 nil 59 | (lambda (public) 60 | (or (and public "public") 61 | "private"))) 62 | (description "Description" 0 nil identity)) 63 | "Format for gist list." 64 | :type '(alist :key-type 65 | (choice 66 | (const :tag "Id" id) 67 | (const :tag "Creation date" created) 68 | (const :tag "Visibility" visibility) 69 | (const :tag "Description" description) 70 | (const :tag "Files" files)) 71 | :value-type 72 | (list 73 | (string :tag "Label") 74 | (integer :tag "Field length") 75 | (boolean :tag "Sortable") 76 | (choice 77 | (string :tag "Format") 78 | (function :tag "Formatter")))) 79 | :group 'gist) 80 | 81 | (defcustom gist-view-gist nil 82 | "If non-nil, view gists with `browse-url' after posting." 83 | :type 'boolean 84 | :group 'gist) 85 | 86 | (defcustom gist-multiple-files-mark "+" 87 | "Symbol to use to indicate gists with multiple files." 88 | :type 'string 89 | :group 'gist) 90 | 91 | (defcustom gist-ask-for-description nil 92 | "If non-nil, prompt for description before submitting gist." 93 | :type 'boolean 94 | :group 'gist) 95 | 96 | (defcustom gist-ask-for-filename nil 97 | "If non-nil, prompt for change default file name before submitting gist." 98 | :type 'boolean 99 | :group 'gist) 100 | 101 | (defcustom gist-created-fmt "Paste created: %s" 102 | "Format for the message that gets shown upon successful gist 103 | creation. Must contain a single %s for the location of the newly 104 | created gist." 105 | :type 'string 106 | :group 'gist) 107 | 108 | (defcustom gist-supported-modes-alist '((action-script-mode . "as") 109 | (c-mode . "c") 110 | (c++-mode . "cpp") 111 | (clojure-mode . "clj") 112 | (common-lisp-mode . "lisp") 113 | (css-mode . "css") 114 | (diff-mode . "diff") 115 | (emacs-lisp-mode . "el") 116 | (lisp-interaction-mode . "el") 117 | (erlang-mode . "erl") 118 | (haskell-mode . "hs") 119 | (html-mode . "html") 120 | (io-mode . "io") 121 | (java-mode . "java") 122 | (javascript-mode . "js") 123 | (jde-mode . "java") 124 | (js2-mode . "js") 125 | (lua-mode . "lua") 126 | (ocaml-mode . "ml") 127 | (objective-c-mode . "m") 128 | (perl-mode . "pl") 129 | (php-mode . "php") 130 | (python-mode . "py") 131 | (ruby-mode . "rb") 132 | (text-mode . "txt") 133 | (scala-mode . "scala") 134 | (sql-mode . "sql") 135 | (scheme-mode . "scm") 136 | (smalltalk-mode . "st") 137 | (sh-mode . "sh") 138 | (tcl-mode . "tcl") 139 | (tex-mode . "tex") 140 | (xml-mode . "xml")) 141 | "Mapping between major-modes and file extensions. 142 | Used to generate filenames for created gists, and to select 143 | appropriate modes from fetched gist files (based on filenames)." 144 | :type '(alist :key-type (symbol :tag "Mode") 145 | :value-type (string :tag "Extension"))) 146 | 147 | (defvar gist-list-db nil) 148 | (unless (hash-table-p gist-list-db) 149 | (setq gist-list-db (make-hash-table :test 'equal))) 150 | 151 | (defvar gist-list-db-by-user nil) 152 | (unless (hash-table-p gist-list-db-by-user) 153 | (setq gist-list-db-by-user (make-hash-table :test 'equal))) 154 | 155 | (defvar gist-list-limits nil) 156 | 157 | (defvar gist-id nil) 158 | (make-variable-buffer-local 'gist-id) 159 | 160 | (defvar gist-filename nil) 161 | (make-variable-buffer-local 'gist-filename) 162 | 163 | (defvar gist-user-history nil "History list for gist-list-user.") 164 | 165 | (defvar gist-list-buffer-user nil "Username for this gist buffer.") 166 | (make-variable-buffer-local 'gist-list-buffer-user) 167 | (put 'gist-list-buffer-user 'permanent-local t) 168 | 169 | (defun gist-get-api (&optional sync) 170 | (let ((gh-profile-current-profile 171 | (or gh-profile-current-profile (gh-profile-completing-read)))) 172 | (make-instance 'gh-gist-api :sync sync :cache t :num-retries 1))) 173 | 174 | (defun gist-internal-new (files &optional private description callback) 175 | (let* ((api (gist-get-api)) 176 | (gist (make-instance 'gh-gist-gist-stub 177 | :public (or (not private) json-false) 178 | :description (or description "") 179 | :files files)) 180 | (resp (gh-gist-new api gist))) 181 | (gh-url-add-response-callback 182 | resp 183 | (lexical-let ((profile (oref api :profile)) 184 | (cb callback)) 185 | (lambda (gist) 186 | (let ((gh-profile-current-profile profile)) 187 | (funcall (or cb 'gist-created-callback) gist))))))) 188 | 189 | (defun gist-ask-for-description-maybe () 190 | (when gist-ask-for-description 191 | (read-from-minibuffer "Gist description: "))) 192 | 193 | (defun gist-ask-for-filename-maybe (fname) 194 | (if gist-ask-for-filename 195 | (read-string (format "File name (%s): " fname) nil nil fname) 196 | fname)) 197 | 198 | ;;;###autoload 199 | (defun gist-region (begin end &optional private callback) 200 | "Post the current region as a new paste at gist.github.com 201 | Copies the URL into the kill ring. 202 | 203 | With a prefix argument, makes a private paste." 204 | (interactive "r\nP") 205 | (let* ((file (or (buffer-file-name) (buffer-name))) 206 | (name (file-name-nondirectory file)) 207 | (ext (or (cdr (assoc major-mode gist-supported-modes-alist)) 208 | (file-name-extension file) 209 | "txt")) 210 | (proposal-fname (concat (file-name-sans-extension name) "." ext)) 211 | (fname (gist-ask-for-filename-maybe proposal-fname)) 212 | (files (list 213 | (make-instance 'gh-gist-gist-file 214 | :filename fname 215 | :content (buffer-substring begin end))))) 216 | (gist-internal-new files private 217 | (gist-ask-for-description-maybe) callback))) 218 | 219 | (defun gist-files (filenames &optional private callback) 220 | (let ((files nil)) 221 | (dolist (f filenames) 222 | (with-temp-buffer 223 | (insert-file-contents f) 224 | (let ((name (file-name-nondirectory f))) 225 | (push (make-instance 'gh-gist-gist-file :filename name :content (buffer-string)) 226 | files)))) 227 | (gist-internal-new files private 228 | (gist-ask-for-description-maybe) callback))) 229 | 230 | (defun gist-created-callback (gist) 231 | (let ((location (oref gist :html-url))) 232 | (gist-list-reload 'current-user t) 233 | (message gist-created-fmt location) 234 | (when gist-view-gist 235 | (browse-url location)) 236 | (kill-new location))) 237 | 238 | ;;;###autoload 239 | (defun gist-region-private (begin end) 240 | "Post the current region as a new private paste at gist.github.com 241 | Copies the URL into the kill ring." 242 | (interactive "r") 243 | (gist-region begin end t)) 244 | 245 | ;;;###autoload 246 | (defun gist-buffer (&optional private) 247 | "Post the current buffer as a new paste at gist.github.com. 248 | Copies the URL into the kill ring. 249 | 250 | With a prefix argument, makes a private paste." 251 | (interactive "P") 252 | (gist-region (point-min) (point-max) private)) 253 | 254 | ;;;###autoload 255 | (defun gist-buffer-private () 256 | "Post the current buffer as a new private paste at gist.github.com. 257 | Copies the URL into the kill ring." 258 | (interactive) 259 | (gist-region-private (point-min) (point-max))) 260 | 261 | ;;;###autoload 262 | (defun gist-region-or-buffer (&optional private) 263 | "Post either the current region, or if mark is not set, the 264 | current buffer as a new paste at gist.github.com 265 | 266 | Copies the URL into the kill ring. 267 | 268 | With a prefix argument, makes a private paste." 269 | (interactive "P") 270 | (if (region-active-p) 271 | (gist-region (point) (mark) private) 272 | (gist-buffer private))) 273 | 274 | ;;;###autoload 275 | (defun gist-region-or-buffer-private () 276 | "Post either the current region, or if mark is not set, the 277 | current buffer as a new private paste at gist.github.com 278 | 279 | Copies the URL into the kill ring." 280 | (interactive) 281 | (if (region-active-p) 282 | (gist-region-private (point) (mark)) 283 | (gist-buffer-private))) 284 | 285 | ;;;###autoload 286 | (defun gist-list-user (username &optional force-reload background) 287 | "Displays a list of a user's gists in a new buffer. When called from 288 | a program, pass 'current-user as the username to view the user's own 289 | gists, or nil for the username and a non-nil value for force-reload to 290 | reload the gists for the current buffer." 291 | (interactive 292 | (let ((username (read-from-minibuffer "GitHub user: " nil nil nil 293 | 'gist-user-history)) 294 | (force-reload (equal current-prefix-arg '(4)))) 295 | (list username force-reload))) 296 | ;; if buffer exists, it contains the current gh profile 297 | (let* ((gh-profile-current-profile (or gh-profile-current-profile 298 | (gh-profile-completing-read))) 299 | (bufname (if (null username) 300 | (if (not (equal major-mode 'gist-list-mode)) 301 | (error "Current buffer isn't a gist-list-mode buffer") 302 | (buffer-name)) 303 | (format "*%s:%sgists*" 304 | gh-profile-current-profile 305 | (if (or (equal "" username) 306 | (eq 'current-user username)) 307 | "" 308 | (format "%s's-" username))))) 309 | (api (gist-get-api nil)) 310 | (username (or (and (null username) gist-list-buffer-user) 311 | (and (not (or (null username) 312 | (equal "" username) 313 | (eq 'current-user username))) 314 | username) 315 | (gh-api-get-username api)))) 316 | (when force-reload 317 | (pcache-clear (oref api :cache)) 318 | (or background (message "Retrieving list of gists..."))) 319 | (unless (and background (not (get-buffer bufname))) 320 | (let ((resp (gh-gist-list api username))) 321 | (gh-url-add-response-callback 322 | resp 323 | (lexical-let ((buffer bufname)) 324 | (lambda (gists) 325 | (with-current-buffer (get-buffer-create buffer) 326 | (setq gist-list-buffer-user username) 327 | (gist-lists-retrieved-callback gists background))))) 328 | (gh-url-add-response-callback 329 | resp 330 | (lexical-let ((profile (oref api :profile)) 331 | (buffer bufname)) 332 | (lambda (&rest args) 333 | (with-current-buffer buffer 334 | (setq gh-profile-current-profile profile))))))))) 335 | 336 | ;;;###autoload 337 | (defun gist-list (&optional force-reload background) 338 | "Displays a list of all of the current user's gists in a new buffer." 339 | (interactive "P") 340 | (gist-list-user 'current-user force-reload background)) 341 | 342 | (defun gist-list-reload (&optional username background) 343 | (interactive) 344 | (gist-list-user username t background)) 345 | 346 | (defun gist-list-redisplay () 347 | (gist-list-user 'current-user)) 348 | 349 | (defun gist-tabulated-entry (gist) 350 | (let* ((data (gist-parse-gist gist)) 351 | (repo (oref gist :id))) 352 | (list repo (apply 'vector data)))) 353 | 354 | (defun gist-lists-retrieved-callback (gists &optional background) 355 | "Called when the list of gists has been retrieved. Displays 356 | the list." 357 | (dolist (g (gethash gist-list-buffer-user gist-list-db-by-user)) 358 | (remhash (oref g :id) gist-list-db)) 359 | (dolist (g gists) 360 | (puthash (oref g :id) g gist-list-db)) 361 | (puthash gist-list-buffer-user gists gist-list-db-by-user) 362 | (gist-list-render (gethash gist-list-buffer-user gist-list-db-by-user) 363 | background)) 364 | 365 | (defun gist--get-time (gist) 366 | (let* ((date (timezone-parse-date (oref gist :date))) 367 | (time (timezone-parse-time (aref date 3)))) 368 | (encode-time (string-to-number (aref time 2)) 369 | (string-to-number (aref time 1)) 370 | (string-to-number (aref time 0)) 371 | (string-to-number (aref date 2)) 372 | (string-to-number (aref date 1)) 373 | (string-to-number (aref date 0)) 374 | (aref date 4)))) 375 | 376 | (defun gist-parse-gist (gist) 377 | "Returns a list of the gist's attributes for display, given the xml list 378 | for the gist." 379 | (let ((repo (oref gist :id)) 380 | (creation (gist--get-time gist)) 381 | (desc (or (oref gist :description) "")) 382 | (public (eq t (oref gist :public))) 383 | (fnames (mapcar (lambda (f) (when f (oref f :filename))) (oref gist :files)))) 384 | (loop for (id label width sort format) in gist-list-format 385 | collect (let ((string-formatter (if (eq id 'created) 386 | 'format-time-string 387 | 'format)) 388 | (value (cond ((eq id 'id) repo) 389 | ((eq id 'created) creation) 390 | ((eq id 'visibility) public) 391 | ((eq id 'description) desc) 392 | ((eq id 'files) fnames)))) 393 | (funcall (if (stringp format) 394 | (lambda (val) 395 | (funcall string-formatter format val)) 396 | format) 397 | value))))) 398 | 399 | ;;;###autoload 400 | (defun gist-fetch (id) 401 | (interactive "sGist ID: ") 402 | (let ((gist nil) 403 | (multi nil) 404 | (prefix (format "*gist-%s*" id)) 405 | (result nil) 406 | (profile (gh-profile-current-profile))) 407 | (setq gist (gist-list-db-get-gist id)) 408 | (let ((api (gist-get-api t))) 409 | (cond ((null gist) 410 | ;; fetch it 411 | (setq gist (oref (gh-gist-get api id) :data)) 412 | (puthash (oref gist :id) gist gist-list-db) 413 | (let* ((user (oref gist :user)) 414 | (gists (push gist (gethash user gist-list-db-by-user)))) 415 | (puthash user gists gist-list-db-by-user))) 416 | ((not (gh-gist-gist-has-files gist)) 417 | (gh-gist-get api gist)))) 418 | (let ((files (oref gist :files))) 419 | (setq multi (< 1 (length files))) 420 | (dolist (f files) 421 | (let ((buffer (get-buffer-create (format "%s/%s" prefix 422 | (oref f :filename)))) 423 | (mode (car (rassoc (file-name-extension (oref f :filename)) 424 | gist-supported-modes-alist)))) 425 | (with-current-buffer buffer 426 | (delete-region (point-min) (point-max)) 427 | (insert (oref f :content)) 428 | (let ((fname (oref f :filename))) 429 | ;; set major mode 430 | (if (fboundp mode) 431 | (funcall mode) 432 | (let ((buffer-file-name fname) 433 | enable-dir-local-variables) 434 | (normal-mode))) 435 | ;; set minor mode 436 | (gist-mode 1) 437 | (setq gist-id id 438 | gist-filename fname 439 | gh-profile-current-profile profile)) 440 | (set-buffer-modified-p nil)) 441 | (setq result buffer)))) 442 | (if multi 443 | (let ((ibuffer-mode-hook nil) 444 | (ibuffer-use-header-line nil) 445 | (ibuffer-show-empty-filter-groups nil)) 446 | (ibuffer t prefix 447 | `((name . ,(regexp-quote (concat prefix "/")))) 448 | nil nil 449 | nil 450 | '((name)))) 451 | (switch-to-buffer-other-window result)))) 452 | 453 | (defun gist-fetch-current () 454 | (interactive) 455 | (gist-fetch (tabulated-list-get-id))) 456 | 457 | (defun gist-fetch-current-noselect () 458 | (interactive) 459 | (let ((win (selected-window))) 460 | (gist-fetch-current) 461 | (select-window win))) 462 | 463 | (defun gist--check-perms-and-get-api (gist errormsg apiflg) 464 | (let* ((api (gist-get-api apiflg)) 465 | (username (gh-api-get-username api)) 466 | (gs (gethash username gist-list-db-by-user))) 467 | (if (not (memq gist gs)) 468 | (user-error errormsg) 469 | api))) 470 | 471 | (defun gist-edit-current-description () 472 | (interactive) 473 | (let* ((id (tabulated-list-get-id)) 474 | (gist (gist-list-db-get-gist id)) 475 | (api (gist--check-perms-and-get-api 476 | gist "Can't edit a gist that doesn't belong to you" t))) 477 | (let* ((old-descr (oref gist :description)) 478 | (new-descr (read-from-minibuffer "Description: " old-descr)) 479 | (g (clone gist 480 | :files nil 481 | :description new-descr)) 482 | (resp (gh-gist-edit api g))) 483 | (gh-url-add-response-callback resp 484 | (lambda (gist) 485 | (gist-list-reload)))))) 486 | 487 | (defun gist-add-buffer (buffer) 488 | (interactive "bBuffer: ") 489 | (let* ((buffer (get-buffer buffer)) 490 | (id (tabulated-list-get-id)) 491 | (gist (gist-list-db-get-gist id)) 492 | (api (gist--check-perms-and-get-api 493 | gist "Can't modify a gist that doesn't belong to you" t)) 494 | (fname (file-name-nondirectory (or (buffer-file-name buffer) 495 | (buffer-name buffer)))) 496 | (g (clone gist :files 497 | (list 498 | (make-instance 'gh-gist-gist-file 499 | :filename fname 500 | :content (with-current-buffer buffer 501 | (buffer-string)))))) 502 | (resp (gh-gist-edit api g))) 503 | (gh-url-add-response-callback resp 504 | (lambda (gist) 505 | (gist-list-reload))))) 506 | 507 | (defun gist-remove-file (fname) 508 | (interactive (list 509 | (completing-read 510 | "Filename to remove: " 511 | (let* ((id (tabulated-list-get-id)) 512 | (gist (gist-list-db-get-gist id))) 513 | (mapcar #'(lambda (f) (oref f :filename)) 514 | (oref gist :files)))))) 515 | (let* ((id (tabulated-list-get-id)) 516 | (gist (gist-list-db-get-gist id)) 517 | (api (gist--check-perms-and-get-api 518 | gist "Can't modify a gist that doesn't belong to you" t)) 519 | (g (clone gist :files 520 | (list 521 | (make-instance 'gh-gist-gist-file 522 | :filename fname 523 | :content nil)))) 524 | (resp (gh-gist-edit api g))) 525 | (gh-url-add-response-callback resp 526 | (lambda (gist) 527 | (gist-list-reload))))) 528 | 529 | (defun gist-kill-current () 530 | (interactive) 531 | (let* ((id (tabulated-list-get-id)) 532 | (gist (gist-list-db-get-gist id)) 533 | (api (gist--check-perms-and-get-api 534 | gist "Can't delete a gist that doesn't belong to you" t))) 535 | (when (yes-or-no-p (format "Really delete gist %s ? " id) ) 536 | (let* ((resp (gh-gist-delete api id))) 537 | (gist-list-reload))))) 538 | 539 | (defun gist-current-url () 540 | "Helper function to fetch current gist url" 541 | (let* ((id (or (and (eq major-mode 'gist-list-mode) 542 | (tabulated-list-get-id)) 543 | (and (boundp 'gist-mode) 544 | gist-mode 545 | gist-id))) 546 | (gist (gist-list-db-get-gist id))) 547 | (oref gist :html-url))) 548 | 549 | (defun gist-print-current-url () 550 | "Display the currently selected gist's url in the echo area and 551 | put it into `kill-ring'." 552 | (interactive) 553 | (kill-new (message (gist-current-url)))) 554 | 555 | (defun gist-browse-current-url () 556 | "Browse current gist on github" 557 | (interactive) 558 | (browse-url (gist-current-url))) 559 | 560 | (defun gist--do-star (id how msg) 561 | (let* ((api (gist-get-api t)) 562 | (resp (gh-gist-set-star api id how))) 563 | (gh-url-add-response-callback resp 564 | (lambda (gist) 565 | (message msg id))))) 566 | 567 | ;;;###autoload 568 | (defun gist-star () 569 | (interactive) 570 | (let ((id (tabulated-list-get-id))) 571 | (gist--do-star id t "Starred gist %s"))) 572 | 573 | ;;;###autoload 574 | (defun gist-unstar () 575 | (interactive) 576 | (let ((id (tabulated-list-get-id))) 577 | (gist--do-star id nil "Unstarred gist %s"))) 578 | 579 | ;;;###autoload 580 | (defun gist-list-starred (&optional background) 581 | "List your starred gists." 582 | (interactive) 583 | (let* ((api (gist-get-api t)) 584 | (resp (gh-gist-list-starred api))) 585 | (gh-url-add-response-callback 586 | resp 587 | (lexical-let ((buffer "*starred-gists*")) 588 | (lambda (gists) 589 | (with-current-buffer (get-buffer-create buffer) 590 | (gist-list-render gists background))))))) 591 | 592 | ;;;###autoload 593 | (defun gist-fork () 594 | "Fork a gist." 595 | (interactive) 596 | (let* ((id (tabulated-list-get-id)) 597 | (api (gist-get-api)) 598 | (resp (gh-gist-fork api id))) 599 | (gh-url-add-response-callback resp 600 | (lambda (gist) 601 | (message "Forked gist %s" id))))) 602 | 603 | (defvar gist-list-menu-mode-map 604 | (let ((map (make-sparse-keymap))) 605 | (set-keymap-parent map tabulated-list-mode-map) 606 | (define-key map "\C-m" 'gist-fetch-current) 607 | (define-key map [tab] 'gist-fetch-current-noselect) 608 | (define-key map "g" 'gist-list-reload) 609 | (define-key map "e" 'gist-edit-current-description) 610 | (define-key map "k" 'gist-kill-current) 611 | (define-key map "+" 'gist-add-buffer) 612 | (define-key map "-" 'gist-remove-file) 613 | (define-key map "y" 'gist-print-current-url) 614 | (define-key map "b" 'gist-browse-current-url) 615 | (define-key map "*" 'gist-star) 616 | (define-key map "^" 'gist-unstar) 617 | (define-key map "f" 'gist-fork) 618 | (define-key map "/p" 'gist-list-push-visibility-limit) 619 | (define-key map "/t" 'gist-list-push-tag-limit) 620 | (define-key map "/w" 'gist-list-pop-limit) 621 | map)) 622 | 623 | (define-derived-mode gist-list-mode tabulated-list-mode "Gists" 624 | "Major mode for browsing gists. 625 | \\ 626 | \\{gist-list-menu-mode-map}" 627 | (setq tabulated-list-format 628 | (apply 'vector 629 | (loop for (sym label width sort format) in gist-list-format 630 | collect (list label width sort))) 631 | tabulated-list-padding 2 632 | tabulated-list-sort-key nil) 633 | (tabulated-list-init-header) 634 | (use-local-map gist-list-menu-mode-map) 635 | (font-lock-add-keywords nil '(("#[^[:space:]]*" . 'font-lock-keyword-face)))) 636 | 637 | (defun gist-list-pop-limit (&optional all) 638 | (interactive "P") 639 | (if all 640 | (setq gist-list-limits nil) 641 | (pop gist-list-limits)) 642 | (gist-list-redisplay)) 643 | 644 | (defun gist-list-push-visibility-limit (&optional private) 645 | (interactive "P") 646 | (push (apply-partially (lambda (flag g) 647 | (or (and flag (not (oref g :public))) 648 | (and (not flag) (oref g :public)))) 649 | private) 650 | gist-list-limits) 651 | (gist-list-redisplay)) 652 | 653 | (defun gist-parse-tags (tags) 654 | (let ((words (split-string tags)) 655 | with without) 656 | (dolist (w words) 657 | (cond ((string-prefix-p "+" w) 658 | (push (substring w 1) with)) 659 | ((string-prefix-p "-" w) 660 | (push (substring w 1) without)) 661 | (t 662 | (push w with)))) 663 | (list with without))) 664 | 665 | (defun gist-list-push-tag-limit (tags) 666 | (interactive "sTags: ") 667 | (let* ((lsts (gist-parse-tags tags)) 668 | (with (car lsts)) 669 | (without (cadr lsts))) 670 | (push (apply-partially (lambda (with without g) 671 | (and 672 | (every (lambda (tag) 673 | (string-match-p 674 | (format "#%s\\>" tag) 675 | (oref g :description))) 676 | with) 677 | (not (some (lambda (tag) 678 | (string-match-p 679 | (format "#%s\\>" tag) 680 | (oref g :description))) 681 | without)))) 682 | with without) 683 | gist-list-limits)) 684 | (gist-list-redisplay)) 685 | 686 | (defun gist-list-apply-limits (gists) 687 | (condition-case nil 688 | (delete nil 689 | (mapcar 690 | (lambda (g) 691 | (when (every #'identity 692 | (mapcar (lambda (f) (funcall f g)) gist-list-limits)) 693 | g)) 694 | gists)) 695 | (error gists))) 696 | 697 | (defun gist-list-render (gists &optional background) 698 | (gist-list-mode) 699 | (let ((entries (mapcar 'gist-tabulated-entry 700 | (gist-list-apply-limits gists)))) 701 | (setq tabulated-list-entries entries) 702 | (when (not (equal (length gists) (length entries))) 703 | (setq mode-name (format "Gists[%d/%d]" (length entries) (length gists))))) 704 | (tabulated-list-print) 705 | (gist-list-tag-multi-files) 706 | (unless background 707 | (set-window-buffer nil (current-buffer)))) 708 | 709 | (defun gist-list-tag-multi-files () 710 | (let ((ids nil)) 711 | (maphash (lambda (k v) 712 | (when (< 1 (length (oref v :files))) 713 | (push (oref v :id) ids))) 714 | gist-list-db) 715 | (save-excursion 716 | (goto-char (point-min)) 717 | (while (not (eobp)) 718 | (if (member (tabulated-list-get-id) ids) 719 | (tabulated-list-put-tag gist-multiple-files-mark t) 720 | (forward-line 1)))))) 721 | 722 | (defun gist-list-db-get-gist (id) 723 | (gethash id gist-list-db)) 724 | 725 | ;;; Gist minor mode 726 | 727 | (defun gist-mode-edit-buffer (&optional new-name) 728 | (when (or (buffer-modified-p) new-name) 729 | (let* ((id gist-id) 730 | (gist (gist-list-db-get-gist id)) 731 | (files (list 732 | (make-instance 'gh-gist-gist-file 733 | :filename (or new-name gist-filename) 734 | :content (buffer-string))))) 735 | (when new-name 736 | ;; remove old file as well 737 | (add-to-list 'files 738 | (make-instance 'gh-gist-gist-file 739 | :filename gist-filename 740 | :content nil))) 741 | (let* ((g (clone gist 742 | :files files)) 743 | (api (gist-get-api t)) 744 | (resp (gh-gist-edit api g))) 745 | (gh-url-add-response-callback 746 | resp 747 | (lambda (gist) 748 | (set-buffer-modified-p nil) 749 | (when new-name 750 | (rename-buffer (replace-regexp-in-string "/.*$" 751 | (concat "/" new-name) 752 | (buffer-name))) 753 | (setq gist-filename new-name)) 754 | (let ((g (gist-list-db-get-gist (oref gist :id)))) 755 | (oset g :files (oref gist :files))))))))) 756 | 757 | (defun gist-mode-save-buffer () 758 | (interactive) 759 | (gist-mode-edit-buffer)) 760 | 761 | (defun gist-mode-write-file () 762 | (interactive) 763 | (let ((new-name (read-from-minibuffer "File name: " gist-filename))) 764 | (gist-mode-edit-buffer new-name))) 765 | 766 | (defvar gist-mode-map 767 | (let ((map (make-sparse-keymap))) 768 | (define-key map [remap save-buffer] 'gist-mode-save-buffer) 769 | (define-key map [remap write-file] 'gist-mode-write-file) 770 | map)) 771 | 772 | (define-minor-mode gist-mode 773 | "Minor mode for buffers containing gists files" 774 | :lighter " gist" 775 | :map 'gist-mode-map) 776 | 777 | ;;; Dired integration 778 | 779 | (require 'dired) 780 | 781 | (defun dired-do-gist (&optional private) 782 | (interactive "P") 783 | (gist-files (dired-get-marked-files) private)) 784 | 785 | (define-key dired-mode-map "@" 'dired-do-gist) 786 | 787 | (provide 'gist) 788 | ;;; gist.el ends here 789 | --------------------------------------------------------------------------------