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.
Read Full Post »