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.
Supposedly, some customer service app that Amazon used was written in emacs. Thus spake Steve Yegge:
http://steve.yegge.googlepages.com/tour-de-babel
And people really liked it.
Hi Seth, thanks for the comment. It would have been nice if Stevey had given more details about Mailman. I can’t imagine an emacs-based app that would be pleasant to use for non-IT people.
Yeah, feel the same.
When I read it, I was really curious about what it did/looked like. That’s probably why it stuck in my noggin and your post jogged it loose. I imagine an interface similar to customize but even that must be off putting (to me at least).
BTW, really like your blog. I’ve been using emacs for about 1.5 years now and have learned quite a bit from your posts. Keep up the great work and thanks for sharing.
I’ve been thinking lately about putting together an emacs ‘distro’ (actually a lightweight Linux distro with Emacs + packages + usable customizations) for use by writers, and I’d love to see a decent dired-substitute file selector. Even though the combination I’m thinking of is still only suitable for people with some willingness to plow through new interfaces, and learn keymappings, and post cheatsheets near their desks, you do want to cut the complexity where you can, especially if it only comes at the expense of functionality which you don’t need at that location in the interface.
So, no, I don’t think it’s crazy, and I will ‘watch this space’.
@Seth – I’m not that keen on customize either.
Thanks for the positive comment on the blog. It really is nice to hear that people find it useful.
@commonman – I put together a distro for my parents to use, mainly so they could appreciate org-mode. Re: the alternative dired I’ve had some other work to do recently but I’m hoping to get back to it shortly. If you’ve any suggestions I’d be interested to hear them.