;;; auto-menu.el --- Auto Menu Functions ;; ;;; Copyright (C) 2007,2008 Kyle W T Sherman ;; ;; Author: Kyle W T Sherman <kylewsherman at gmail dot com> ;; Created: 2007-05-22 ;; Version: 1.0 ;; Keywords: menu ;; ;; This file is not part of GNU Emacs. ;; ;; This 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, or (at your option) any later version. ;; ;; This 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 GNU Emacs; see the file COPYING. If not, write to the Free Software ;; Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. ;; ;;; Commentary: ;; ;; Provides `auto-menu', `auto-menu-select', `auto-menu-file-dir', ;; `auto-menu-dired', and `auto-menu-dired-remote' functions to help creating ;; Emacs menus. ;; ;;; Installation: ;; ;; Put `auto-menu.el' where you keep your elisp files and add something like ;; the following to your .emacs file: ;; ;; (require 'auto-menu) ;; ;;; Usage: ;; ;; The `auto-menu' function creates a new menu. You pass it a name and a list ;; of items. Each item is either: 1) a list contianing a name, function, and ;; help text, or 2) a sub-menu containing a name and a list of items. This ;; functionality recurses so you can have n-depth sub-menus. ;; ;; The `auto-menu-select' function is similar to `auto-menu' except that it ;; opens the menu in a buffer and prompts the user to select an item. It also ;; does not use keymaps and can handle lambda functions. This function is good ;; to use in other functions that need to prompt the user with a list of ;; options to select from. ;; ;; The `auto-menu-file-dir' function creates a new menu based on a given ;; directory. It creates an entry for every file matching a pattern (defaults ;; to `.*') and applies a function to it (defaults to `find-file'). There is ;; also an option to recurse through sub-directories or not (defaults to no). ;; ;; The `auto-menu-file' function creates a new menu of `find-file' commands. ;; It creates an entry for every file given. ;; ;; The `auto-menu-dired' function creates a new menu of `dired' commands. It ;; creates an entry for every directory given. ;; ;; The `auto-menu-dired-remote' function creates a new menu of `dired' ;; commands to connect with remote servers. It takes a list of servers and ;; users and creates a menu of servers that each lead to sub-menus of users ;; that when selected will open a dired buffer at that location. ;; ;; Some examples from my configuration (edited for size): ;; ;; ;; dired menu ;; (auto-menu ;; "Dired" ;; (auto-menu-dired '(("home" . "~/") ;; (".emacs.d" . "~/.emacs.d") ;; (".elisp" . "~/.elisp") ;; ("clojure" . "~/clojure") ;; ("clisp" . "~/clisp") ;; ("bin" . "~/bin")))) ;; ;; ;; load menu ;; (auto-menu ;; "Load" ;; `(("Restore Context" "(context-restore)" "Restore previous context save.") ;; ("Home Files..." ;; ,(auto-menu-file '((".profile" . "~/.profile") ;; (".bashrc" . "~/.bashrc")))) ;; ("Emacs Settings..." ;; ,(append '((".emacs" "(find-file \"~/.emacs\")" "Load `~/.emacs' file.")) ;; (auto-menu-file-dir "~/.emacs.d" "\\.el$" "find-file"))) ;; ("Elisp Files..." ;; ,(auto-menu-file-dir "~/.emacs.d" "\\.el$" "find-file" t)) ;; ("Clojure Files..." ;; ,(auto-menu-file-dir "~/clojure" "\\.clj$" "find-file" t)) ;; ("CLisp Files..." ;; ,(auto-menu-file-dir "~/clisp" "\\.lisp$" "find-file" t)) ;; ("Org Files..." ;; ,(auto-menu-file-dir "~/org" "\\.\\(org\\|org\\.cpt\\)$" "find-file" t)))) ;; ;; ;; run menu ;; (auto-menu ;; "Run" ;; '(("Emacs Server" "server-start-maybe" "Restart Emacs server.") ;; ("Visit TAGS" "(when (file-exists-p \"~/TAGS\") (visit-tags-table \"~/TAGS\"))" "Visit tags table.") ;; ("IELM Mode" "ielm" "Open buffer for interactively evaluating Emacs Lisp expressions.") ;; ("SLIME Mode" "slime" "Start SLIME mode for interactively evaluating CLISP expressions.") ;; ("Evaluate Current Buffer" "eval-buffer" "Run eval-buffer on the current buffer.") ;; ("Evaluate Current SLIME Buffer" "slime-eval-buffer" "Run slime-eval-buffer on the current buffer.") ;; ("Compile ~/.elisp Directory" "compile-elisp" "Byte compile `~/.elisp' directory.") ;; ("Customize Group" "customize-group" "Run customize group function.") ;; )) ;; ;; ;; TODO: add auto-menu-select examples here ;; (auto-menu-select "test" `(("item1" ,(lambda () (message "test1"))) ("item2" ,(lambda () (message "test2"))) ("version" ,(version)))) ;; (auto-menu-select "test" `(("menu1" (("item1" ,(lambda () (message "test1"))) ("item2" ,(lambda () (message "test2"))))))) ;;; Code: ;; easymenu (require 'easymenu) ;; auto menu sanatize (defun auto-menu-sanatize (name) "Return a sanatized version of NAME. Spaces are converted to dashes and anything not in this string is removed: abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" (let ((legal-regexp "[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ012\ 3456789_-]")) (map 'string (lambda (x) x) (cl-loop for x across name if (string-match legal-regexp (char-to-string x)) collect x else if (= x 32) collect 45)))) ;; auto menu ;;;###autoload (defun auto-menu (name items &optional submenu) "Create a menu called NAME consisting of ITEMS and add it to the menu bar. ITEMS is a list of items. An ITEM is either a list containing the following elements that define a menu item: NAME is the menu item name. FUNCTION is a string containing a function name or a function definition (without the interactive call). HELP is the help text. or an ITEM is a named list (a string and a list) defining a sub-menu where: NAME is the sub-menu name. ITEMS is a list of items. SUBMENU is for internal use." (let* ((lname (auto-menu-sanatize (downcase name))) (fname (if submenu (concat submenu "-" lname) (concat "auto-menu-function-" lname))) (menu-name (concat "auto-menu-" lname "-menu")) (menu-map-name (concat "auto-menu-" lname "-menu-map")) (menu "")) ;; add menu node (setq menu (concat menu " (list \"" name "\"\n")) ;; loop through each item in items (dolist (item items) (let ((item-name (car item)) (item-fname (concat fname "-" (auto-menu-sanatize (downcase (car item))))) (item-thing (cadr item)) (item-help (caddr item))) ;; if item is a submenu, add it and recursively call (if (listp item-thing) (setq menu (concat menu (auto-menu item-name item-thing fname))) ;; otherwise, add menu item (let ((funct (if (commandp (intern item-thing)) ;; if item is an interactive function use it directly item-thing (progn ;; otherwise, create an interactive function (eval (read (concat "(defun " item-fname " ()" " \"Auto menu function for `" item-name "'.\"" " (interactive)" " " (if (string= (substring item-thing 0 1) "(") item-thing (concat "(" item-thing ")")) ")"))) item-fname)))) ;; add item to the menu (setq menu (concat menu " [\"" item-name "\" " funct (if item-help (concat " :help \"" item-help "\"") "") "]\n")))))) ;; close menu node (setq menu (concat menu " )\n")) ;; return from submenu or finialize result (if submenu ;; return submenu menu ;; otherwise, add header and footer code and return full result (progn ;; create menu map var (eval (read (concat "(defvar " menu-map-name " nil \"" name " menu map.\")"))) ;; create menu const (eval (read (concat "(defconst " menu-name "\n" menu ")"))) ;; create menu map (eval (read (concat "(easy-menu-define " menu-map-name " nil " "\"Auto menu for `" name "'.\"" " " menu-name ")"))) ;; remove menu item if it currently exists, then add it back (eval (read (concat "(easy-menu-remove-item (current-global-map) '(\"menu-bar\") \"" menu-map-name "\")"))) (eval (read (concat "(easy-menu-add-item (current-global-map) '(\"menu-bar\") " menu-map-name ")"))) ;; return t t)))) ;; auto menu select buffer name (defvar auto-menu-select-buffer-name "*Auto-Menu-Select*" "Buffer name to use for select menu.") ;; ;; auto menu select text property ;; (defconst auto-menu-select-text-property ;; "auto-menu-select-action" ;; "Name of text property to store menu action.") ;; ;; auto menu select mode map ;; (defvar auto-menu-select-mode-map ;; (let ((map (make-sparse-keymap))) ;; (define-key map "\r" 'auto-menu-select-item) ;; map)) ;; ;; auto menu select mode ;; (defun auto-menu-select-mode () ;; "Major mode for displaying auto menu select menus." ;; ;;(interactive) ;; (kill-all-local-variables) ;; (setq major-mode 'auto-menu-select-mode) ;; (setq mode-name "Auto Menu Select") ;; (use-local-map auto-menu-select-mode-map) ;; (setq buffer-read-only t) ;; (run-hooks 'auto-menu-select-mode-hook)) ;; (defun auto-menu-select-mode-move (direction) ;; "Move to next menu item in select menu. ;; If DIRECTION is a positive integer, move that many items forward. ;; If DIRECTION is a negative integer, move that many items backward. ;; DIRECTION defaults to 1." ;; ;; TODO: code this ;; (goto-char (point-at-bol)) ;; (set-mark (point)) ;; (goto-char (point-at-eol))) ;; ;; auto menu select print ;; (defun auto-menu-select-print (name items) ;; "Print menu called NAME of ITEMS. ;; Imbed item actions in text properties." ;; ;;(interactive "*") ;; ;; make sure cursor is at the start of a new line ;; (when (not (point-at-bol)) ;; (goto-char (point-at-eol)) ;; (newline)) ;; ;; add header ;; (insert (concat "= " name " =")) ;; (newline) ;; (newline) ;; ;; interrogate items and print menu ;; (dolist (item items) ;; (let ((name (car item)) ;; (item (cadr item)) ;; action) ;; (if (and (listp item) ;; (listp (car item)) ;; (stringp (caar item)) ;; (listp (cadar item))) ;; ;; sub-menu ;; (setq action `(lambda () (auto-menu-select ,name (quote ,item)))) ;; ;; everything else is just added as-is ;; (setq action item)) ;; ;; add item name and properties to menu ;; (let ((beg (point))) ;; (insert name) ;; (add-text-properties beg (point) ;; (list auto-menu-select-text-property ;; action)) ;; (newline))))) ;; ;; auto menu select item ;; (defun auto-menu-select-item () ;; "Get ACTION from text property ;; `auto-menu-select-text-property', kill current (menu) buffer, ;; then execute ACTION." ;; (interactive) ;; ;; get properties ;; (let ((action (get-text-property (point) auto-menu-select-text-property))) ;; ;;(message "action: %S" action) ;; ;; continue if action looks good ;; (when (fboundp action) ;; ;; kill menu buffer ;; (kill-buffer nil) ;; ;; perform action ;; (funcall action)))) ;; ;; auto menu select ;; (defun auto-menu-select (name items) ;; "Create a menu called NAME consisting of ITEMS and prompt the ;; user to select one. ;; ITEMS is a list of items. An ITEM is either a list containing the following ;; elements that define a menu item: ;; NAME is the menu item name. ;; SYMBOL either a VARIABLE, FUNCTION, or STRING. ;; and SYMBOL is one of: ;; VARIABLE is a lisp variable. ;; FUNCTION is either a function name or a lambda definition. ;; STRING is a string to be returned verbatim. ;; or an ITEM is a named list (a string and a list) defining a ;; sub-menu where: ;; NAME is the sub-menu name. ;; ITEMS is a list of items." ;; (interactive) ;; (let (buffer) ; menu buffer ;; ;; setup auto-menu-select-buffer-name buffer ;; (setq buffer (generate-new-buffer auto-menu-select-buffer-name)) ;; (set-buffer buffer) ;; ;;(setq buffer-read-only nil) ;; ;;(erase-buffer) ;; ;; call auto menu print function ;; (auto-menu-select-print name items) ;; ;; set auto menu buffer to read-only ;; (setq buffer-read-only t) ;; ;; more setup ;; (switch-to-buffer buffer) ;; (goto-char (point-min)) ;; (auto-menu-select-mode) ;; (forward-line 2))) ;; ;; auto menu select print ;; (defun auto-menu-select-print (name items) ;; "Print menu called NAME of ITEMS." ;; ;;(interactive "*") ;; ;; make sure cursor is at the start of a new line ;; (when (not (point-at-bol)) ;; (widget-insert "\n")) ;; ;; add header ;; (widget-insert (concat "= " name " =\n\n")) ;; ;; interrogate items and print menu ;; (dolist (item items) ;; (let ((name (car item)) ;; (item (cadr item)) ;; action) ;; (if (and (listp item) ;; (listp (car item)) ;; (stringp (caar item)) ;; (listp (cadar item))) ;; ;; sub-menu ;; (setq action `(lambda () (auto-menu-select ,name (quote ,item)))) ;; ;; everything else is just added as-is ;; (setq action item)) ;; ;; create and add widget to menu ;; ;;(widget-insert " ") ;; (widget-create 'push-button ;; :value name ;; :notify (lambda (&rest ignore) ;; (kill-buffer nil) ;; (funcall action))) ;; (widget-insert "\n")))) ;; auto menu select ;;;###autoload (defun auto-menu-select (name items) "Create a menu called NAME consisting of ITEMS, and prompt the user to select one. ITEMS is a list of items. An ITEM is either a list containing the following elements that define a menu item: NAME is the menu item name. SYMBOL either a VARIABLE, FUNCTION, or STRING. and SYMBOL is one of: VARIABLE is a lisp variable. FUNCTION is either a function name or a lambda definition. STRING is a string to be returned verbatim. or an ITEM is a named list (a string and a list) defining a sub-menu where: NAME is the sub-menu name. ITEMS is a list of items." (interactive) (let (buffer) ; menu buffer ;; setup auto-menu-select-buffer-name buffer (setq buffer (generate-new-buffer auto-menu-select-buffer-name)) (set-buffer buffer) (kill-all-local-variables) ;;(setq buffer-read-only nil) ;;(erase-buffer) ;; add header (widget-insert (concat name "\n\n")) ;; interrogate items and print menu (dolist (item items) (let ((name (car item)) (item (cadr item)) action) (if (and (listp item) (listp (car item)) (stringp (caar item)) (listp (cadar item))) ;; sub-menu (setq action `(lambda () (auto-menu-select ,name (quote ,item)))) ;; everything else is just added as-is (setq action item)) ;; create and add widget to menu ;;(widget-insert " ") (widget-create 'push-button :value name :notify `(lambda (&rest ignore) (kill-buffer nil) (if (fboundp ,action) (funcall ,action) ,action))) (widget-insert "\n"))) ;; final setup ;;(setq buffer-read-only t) (use-local-map widget-keymap) (widget-setup) (switch-to-buffer buffer) (goto-char (point-min)) (widget-forward 1))) ;; auto menu file dir ;;;###autoload (defun auto-menu-file-dir (dir &optional match funct recurse updir) "Return an auto-menu items list containing an item for every file in DIR that matches the regexp MATCH (defaults to `.*') with FUNCT applied to it (defaults to `find-file'). If RECURSE is non-nil sub-directories will be recursed (defaults to nil). UPDIR is for internal use." ;;(interactive "DDirectory: ") ;; expand dir to full path (setq dir (expand-file-name (file-truename dir))) ;; make sure dir is a directory ;; (unless (file-directory-p dir) ;; (error (format "`%s' is not a directory" dir))) (when (file-directory-p dir) (let ((match (or match ".*")) (funct (or funct "find-file")) items ; items list to populate (files (directory-files dir t))) ; files in dir ;; loop through files (dolist (file files) ;; is item accessable? (when (file-readable-p file) (let ((base-name (file-name-nondirectory file))) ;; branch on type of item (cond ;; ignore `.' and `..' ((string-match "^\\.\\.?$" base-name) t) ;; directory ((file-directory-p file) (when recurse ;; ignore `.git' and `.svn' (when (not (or (string-match "^\\.git$" base-name) (string-match "^\\.svn$" base-name))) (push (list (concat base-name " (dir)") (auto-menu-file-dir file match funct recurse dir)) items))) t) ;; matching file (add to menu) ((string-match match file) ;;(let ((file-name (file-name-sans-extension base-name))) (let ((file-name base-name)) ;; add menu item (push (list file-name (concat "(" funct " \"" file "\")") (concat "Apply `" funct "' to `" file "' file.")) items)) t))))) (nreverse items)))) ;; TODO: refactor the following two functions into a macro that generates them ;; auto menu file ;;;###autoload (defun auto-menu-file (files) "Return an auto-menu items list containing an item for every file in FILES. FILES is either a list of files an association list containing name/file pairs in this format: ((NAME . FILE) ...)" (if (listp (car files)) ;; handle alist (mapcar (lambda (item) (let ((name (car item)) (file (file-truename (cdr item)))) (list name (concat "(find-file \"" file "\")") (concat "Load `" file "' file.")))) files) ;; handle list (mapcar (lambda (item) (let ((file (file-truename (car item)))) (list file (concat "(find-file \"" file "\")") (concat "Load `" file "' file.")))) files))) ;; auto menu dired ;;;###autoload (defun auto-menu-dired (dirs) "Return an auto-menu items list containing an item for every directory in DIRS. DIRS is either a list of directories or an association list containing name/directory pairs in this format: ((NAME . DIR) ...)" (if (listp (car dirs)) ;; handle alist (mapcar (lambda (item) (let ((name (car item)) (dir (file-truename (cdr item)))) (list name (concat "(dired \"" dir "\")") (concat "Open dired buffer at `" dir "'.")))) dirs) ;; handle list (mapcar (lambda (item) (let ((dir (file-truename (car item)))) (list dir (concat "(dired \"" dir "\")") (concat "Open dired buffer at `" dir "'.")))) dirs))) ;; auto menu dired remote ;;;###autoload (defun auto-menu-dired-remote (users servers) "Return an auto-menu items list containing a submenu for every server directory in SERVERS, each of which contains a list of USERS. USERS is an association list of users and directories in this format: ((NAME . DIR) (NAME . (DIR1 DIR2 ...)) SERVERS is a list of servers." (let (result) (dolist (server servers) (push (list server (cl-do* ((user-dir users (cdr user-dir)) (user (caar user-dir) (caar user-dir)) (dir (cdar user-dir) (cdar user-dir)) result) ((not user-dir) (nreverse result)) (unless (listp dir) (setq dir (list dir))) (dolist (d dir) (push (list (concat user "@" server) (concat "(dired \"/" user "@" server ":" d "\")") (concat "Open dired buffer at `" user "@" server ":" d "'.")) result)))) result)) (nreverse result))) ;;; Tests: ;; (easy-menu-remove-item (current-global-map) '("menu-bar") "$$$Test1$$$") ;; (auto-menu ;; "$$$Test1$$$" ;; '(("Test" "(message \"Hello Test\")" "Print msg to minibuffer."))) ;; (easy-menu-remove-item (current-global-map) '("menu-bar") "$$$Test1$$$") ;; (easy-menu-remove-item (current-global-map) '("menu-bar") "$$$Test2$$$") ;; (auto-menu ;; "$$$Test2$$$" ;; '(("SubMenu" ;; (("Test" "(message \"Hello Test\")" "Print msg to minibuffer."))))) ;; (easy-menu-remove-item (current-global-map) '("menu-bar") "$$$Test2$$$") ;; (auto-menu-map ;; "$$$Test1$$$" ;; '(("SubMenu" ;; (("Test" "(message \"Hello Test\")" "Print msg to minibuffer."))))) ;; (easy-menu-remove-item (current-global-map) '("menu-bar") "$$$Test2$$$") ;; (auto-menu-map ;; "$$$Test2$$$" ;; '(("SubMenu" ;; (("Test" "(message \"Hello Test\")" "Print msg to minibuffer.")))) ;; t) ;; (easy-menu-remove-item (current-global-map) '("menu-bar") "$$$Test2$$$") (provide 'auto-menu) ;;; auto-menu.el ends here