Posts Tagged ‘program design’

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:

(if file-editor-mode-map
  (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)))
     (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))
    (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
        (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))

        (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 ()
  (let ((buffer (get-buffer "*file-editor-dir-list*")))
    (if buffer (switch-to-buffer buffer) (file-editor-default-dirs))))


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 »


Get every new post delivered to your Inbox.