For many of the tasks I have to do day to day, I have a default tool. If I want to edit some text I’ll use Emacs; if I want to write an application slowly that runs quickly I’ll use C++; if I want to write an application quickly that runs slowly I’ll use Perl. And if I have to look after a hundred different configuration files for a hundred different users I’ll write a web interface so they can maintain the files themselves.
Ultimately, with any web app I write, anything complex is entered through a <textarea>. However, I often think that a sufficiently restricted emacs would give the users a nicer experience. If it was for me, I would just add the necessary functionality to dired. However, for a non-IT person, the dired interface is not reasonable.
So, a little experiment – can I make something emacs-based that a non-IT person would be happy to use?
File Selector Design Outline
What I envisage is a file selector that remembers the files that a user has opened before (fairly standard selector functionality right?). When a file is selected it provides a simplified view of the configuration file to the user. When they select save, it will save the full complex configuration file in all its glory.
I can also enforce some policies such as every save will check-in to source code control. Then if my editor doesn’t work correctly or the user does something they didn’t want to do, I can retrieve an earlier, working version.
File Selector Implementation
I had better give the user some defaults to click on to start with. Fortunately the configuration files are stored in two main areas: /data/sales and /data/admin.
(require 'button) (require 'derived) (defconst file-editor-default-dirs (mapcar (lambda (e) (concat "/data" (symbol-name e))) '(sales admin)))
I don’t want to display any files or directories that the users should not be looking in so I exclude them with a regex.
(defvar file-editor-exclude-file-regex "^RCS\\|^#\\|\\.back$\\|~$")
Each line in the file selector will be a button which enters the directory or opens the file respectively. I’ve mentioned emacs buttons previously).
(define-button-type 'open-dir 'action 'file-editor-open-dir 'follow-link t 'help-echo "Open Directory") (define-button-type 'open-file 'action 'file-editor-open-file 'follow-link t 'help-echo "Open Configuration File")
The configuration files are based on xml. I want to redefine some of the keys, such as C-s for save and C-f for search so I derive a major mode from xml-mode.
(define-derived-mode file-editor-mode xml-mode "File Editor" "Major mode for editing configuration files. Special commands: \\{file-editor-mode-map}") (if file-editor-mode-map nil (setq file-editor-mode-map (make-sparse-keymap)) (define-key file-editor-mode-map (kbd "C-f") 'isearch-forward) (define-key file-editor-mode-map (kbd "C-o") 'file-editor-file-selector) (define-key file-editor-mode-map (kbd "C-s") 'file-editor-save-file))
The directory buttons and file buttons call file-editor-open-dir and file-editor-open-file respectively.
(defun file-editor-open-dir (button) (let ((parent (button-get button 'parent)) (dir (button-get button 'dir))) (file-editor-dir-list (format "%s%s" (if parent (concat parent "/") "") dir)))) (defun file-editor-open-file (button) (let ((parent (button-get button 'parent)) (file (button-get button 'file))) (find-file (concat parent "/" file)) (file-editor-mode) (longlines-mode 1)))
When the file selector is first opened, it displays some default directories. Later on I’ll extend this to display files that have been opened recently.
(defun file-editor-insert-opendir-button (parent dir) (insert-text-button (format "[%s]" dir) :type 'open-dir 'parent parent 'dir dir) (insert "\n")) (defun file-editor-default-dirs () (let ((buffer (get-buffer-create "*file-editor-dir-list*"))) (with-current-buffer buffer (progn (erase-buffer) (insert "*** Default File List ***\n\n") (dolist (dir file-editor-default-dirs) (file-editor-insert-opendir-button nil dir))))))
I keep the file selector buffer read-only and therefore need to set inhibit-read-only to t whenever I write to it. I can then get all the files and directories within the current directory using directory-files-and-attributes.
I skip all the files beginning with a period. A string is a type of array so I can just compare against the first character using aref. I suspect it is more efficient than using a regex (surely it must be?) but I haven’t measured.
I list all of the directories prior to the files.
(defun file-editor-dir-list (parent) (let ((buffer (get-buffer-create "*file-editor-dir-list*"))) (with-current-buffer buffer (let ((inhibit-read-only t) files dirs) (setq parent (expand-file-name parent)) (erase-buffer) (dolist (vec (directory-files-and-attributes parent)) (let ((filename (car vec)) (is-directory (cadr vec))) (unless (or (eq (aref filename 0) ?.) (string-match file-editor-exclude-file-regex filename)) (if is-directory (push filename dirs) (push filename files))))) (insert (format "Current Directory: %s\n\n" parent)) (file-editor-insert-opendir-button parent "..") (dolist (dir (reverse dirs)) (file-editor-insert-opendir-button parent dir)) (dolist (file (reverse files)) (insert-text-button file 'parent parent 'file file :type 'open-file) (insert "\n")) (toggle-read-only 1)))))
file-editor-file-selector opens either the default files/directories or the buffer which should contain the last visited location.
(defun file-editor-file-selector () (interactive) (let ((buffer (get-buffer "*file-editor-dir-list*"))) (if buffer (switch-to-buffer buffer) (file-editor-default-dirs)))) (file-editor-file-selector)
Okay that is probably enough for one post. Obviously I have a fair amount of functionality left to implement. What do you guys think? Am I crazy to even consider using emacs over <textarea>? Let me know in the comments.