;; cheat.el ;; Time-stamp: <2007-08-22 10:00:04 sjs> ;; ;; Copyright (c) 2007 Sami Samhuri ;; ;; See http://sami.samhuri.net/2007/08/10/cheat-from-emacs for updates. ;; ;; License ;; ;; This program is free software; you can redistribute it and/or ;; modify it under the terms of the GNU General Public License ;; as published by the Free Software Foundation; either version 2 ;; of the License, or (at your option) any later version. ;; ;; This program is distributed in the hope that it will be useful, ;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;; GNU General Public License for more details. ;; ;; You should have received a copy of the GNU General Public License ;; along with this program; if not, write to the Free Software ;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ;; ;; ;; Provide a handy interface to cheat. ;; See http://cheat.errtheblog.com for details on cheat itself. ;; ;; sjs 2007.08.21 ;; * Cache the list of cheat sheets, update it once a day (configurable). ;; * Strictly complete cheat sheet names. (defvar *cheat-host* "cheat.errtheblog.com") (defvar *cheat-port* "80") (defvar *cheat-uri* (concat *cheat-host* ":" *cheat-port*)) (defvar *cheat-directory* "~/.cheat") (defvar *cheat-sheets-cache-file* (concat *cheat-directory* "/sheets")) (defvar *cheat-last-sheet* nil "Name of the most recently viewed cheat sheet.") (defvar *cheat-sheet-history* nil "List of the most recently viewed cheat sheets.") (defconst +seconds-per-day+ 86400) (defvar *cheat-cache-ttl* +seconds-per-day+ "The minimum age of a stale cache file, in seconds.") ;;; interactive functions (defun cheat (name &optional silent) "Show the specified cheat sheet. If SILENT is non-nil then do not print any output, but return it as a string instead." (interactive (list (cheat-read-sheet-name))) (if silent (cheat-command-silent name) (cheat-command name))) (defun cheat-sheets () "List all cheat sheets." (interactive) (cheat-command "sheets")) (defun cheat-recent () "Show recently added cheat sheets." (interactive) (cheat-command "recent")) (defun cheat-clear-cache () "Clear the local cheat cache, located in ~/.cheat." (interactive) (cheat-command "--clear-cache") (make-directory *cheat-directory*)) (defun cheat-versions (name) "Version history of the specified cheat sheet." (interactive (list (cheat-read-sheet-name))) (cheat-command name "--versions")) (defun cheat-diff (name version) "Show the diff between the given version and the current version of the named cheat. If VERSION is of the form m:n then show the diff between versions m and n." (interactive (list (cheat-read-sheet-name) (read-string "Cheat version(s): "))) (cheat-command name "--diff" version)) (defun cheat-add-current-buffer (name) "Add a new cheat with the specified name and the current buffer as the body." (interactive "sCheat name: \n") (post-cheat name (buffer-string) t) (if (interactive-p) (print (concat "Cheat added (" name ")")))) (defun cheat-edit (name) "Fetch the named cheat and open a buffer containing its body. The cheat can be saved with `cheat-save-current-buffer'." (interactive (list (cheat-read-sheet-name))) (cheat-clear-cache name) ; make sure we're working with the latest version (switch-to-buffer (get-buffer-create (cheat->buffer name))) (insert (cheat-body name)) (if (interactive-p) (print "Run `cheat-save-current-buffer' when you're done editing."))) (defun cheat-save-current-buffer () "Save the current buffer using the buffer name for the title and the contents as the body." (interactive) (let ((name (buffer->cheat (buffer-name (current-buffer))))) (post-cheat name (buffer-string)) ;; TODO check for errors and kill the buffer on success (if (interactive-p) (print (concat "Cheat saved (" name ")"))) (cheat-clear-cache name) (cheat name))) ;;; helpers ;; this is from rails-lib.el in the emacs-rails package (defun string-join (separator strings) "Join all STRINGS using SEPARATOR." (mapconcat 'identity strings separator)) (defun blank (thing) "Return T if THING is nil or an empty string, otherwise nil." (or (null thing) (and (stringp thing) (= 0 (length thing))))) (defun cheat-command (&rest rest) "Run the cheat command with the given arguments, display the output." (interactive "sArguments for cheat: \n") (shell-command (concat "cheat " (string-join " " rest)))) (defun cheat-command-to-string (&rest rest) "Run the cheat command with the given arguments and return the output as a string. Display nothing." (shell-command-to-string (concat "cheat " (string-join " " rest)))) (defalias 'cheat-command-silent 'cheat-command-to-string) (defun cheat-read-sheet-name (&optional prompt) "Get the name of an existing cheat sheet, prompting with completion and history. The name of the sheet read is stored in *cheat-last-sheet* unless it was blank." (let* ((default (when (blank prompt) *cheat-last-sheet*)) (prompt (or prompt (if (not (blank default)) (concat "Cheat name (default: " default "): ") "Cheat name: "))) (name (completing-read prompt (cheat-sheets-list t) nil t nil '*cheat-sheet-history* default))) (when (not (blank name)) (setq *cheat-last-sheet* name)) name)) (defun cheat-sheets-list (&optional fetch-if-missing-or-stale) "Get a list of all cheat sheets. Return the cached list in *cheat-sheets-cache-file* if it's readable and `cheat-cache-stale-p' returns nil. When there is no cache or a stale cache, and FETCH-IF-MISSING-OR-STALE is non-nil, cache the list and then return it. Otherwise return nil." (cond ((and (file-readable-p *cheat-sheets-cache-file*) (not (cheat-cache-stale-p))) (save-excursion (let* ((buffer (find-file *cheat-sheets-cache-file*)) (sheets (split-string (buffer-string)))) (kill-buffer buffer) sheets))) (fetch-if-missing-or-stale (cheat-cache-list) (cheat-sheets-list)) (t nil))) (defun cheat-fetch-list () "Fetch a fresh list of all cheat sheets." (nthcdr 3 (split-string (cheat-command-to-string "sheets")))) (defun cheat-cache-list () "Cache the list of cheat sheets in *cheat-sheets-cache-file*. Return the list." (when (not (file-exists-p *cheat-directory*)) (make-directory *cheat-directory*)) (save-excursion (let ((buffer (find-file *cheat-sheets-cache-file*)) (sheets (cheat-fetch-list))) (insert (string-join "\n" sheets)) (basic-save-buffer) (kill-buffer buffer) sheets))) (defun cheat-cache-stale-p () "Non-nil if the cache in *cheat-sheets-cache-file* is more than *cheat-cache-ttl* seconds old.q If the cache file does not exist then it is considered stale. Also see `cheat-cache-sheets'." (or (null (file-exists-p *cheat-sheets-cache-file*)) (let* ((now (float-time (current-time))) (last-mod (float-time (sixth (file-attributes *cheat-sheets-cache-file*)))) (age (- now last-mod))) (> age *cheat-cache-ttl*)))) (defun cheat-body (name) "Call out to Ruby to load the YAML and return just the body." (shell-command-to-string (concat "ruby -ryaml -e '" "puts YAML.load_file(File.expand_path(\"~/.cheat/" name ".yml\")).to_a[0][-1]'"))) (defun url-http-post (url args) "Send ARGS to URL as a POST request." (let ((url-request-method "POST") (url-request-extra-headers '(("Content-Type" . "application/x-www-form-urlencoded"))) (url-request-data (concat (mapconcat (lambda (arg) (concat (url-hexify-string (car arg)) "=" (url-hexify-string (cdr arg)))) args "&") "\r\n"))) ;; `kill-url-buffer' to discard the result ;; `switch-to-url-buffer' to view the results (debugging). (url-retrieve url 'kill-url-buffer))) (defun kill-url-buffer (status) "Kill the buffer returned by `url-retrieve'." (kill-buffer (current-buffer))) (defun switch-to-url-buffer (status) "Switch to the buffer returned by `url-retreive'. The buffer contains the raw HTTP response sent by the server." (switch-to-buffer (current-buffer))) (defun post-cheat (title body &optional new) (let ((uri (concat "http://" *cheat-uri* "/w/" (if new "" title)))) (url-http-post uri `(("sheet_title" . ,title) ("sheet_body" . ,body) ("from_gem" . "1"))))) (defun buffer->cheat (name) (substring name 7 (- (length name) 1))) (defun cheat->buffer (name) (concat "*cheat-" name "*")) (provide 'cheat)