Posts Tagged ‘file selector’

Storing Session History

A little while ago we were talking about writing a little emacs-based application to enable the users to help themselves. The beginning of this tool needs a light-weight dired using emacs buttons to use for navigating around the filesystem. Today we will look at adding functionality to remember which files and directories have been accessed previously.

First of all we need some variables to store the directories and files in.

(defvar file-editor-current-dir nil)

(defvar file-editor-save-dirs nil)
(defvar file-editor-save-dirs '(a b c))
(defvar file-editor-save-files nil)

Then we provide a customizable variable where the history will be saved between emacs sessions.

(defcustom file-editor-history-file "~/.file-editor-history"
  "File in which the file-editor history is saved between invocations.
Variables stored are: `file-editor-save-dirs', `file-editor-save-files'."
  :type 'string
  :group 'file-editor)

We will frequently be adding the same file and directory into the lists and we don’t want to get dupes. I could use a data structure that helps avoid dupes or I could just sort the lists and remove adjacent dupes. Guess which option I chose.

(defun remove-dupes (list)
  (let (tmp-list head)
    (while list
      (setq head (pop list))
      (unless (equal head (car list))
        (push head tmp-list)))
    (reverse tmp-list)))

(defun file-editor-sort-history ()
  (setq file-editor-save-dirs
        (remove-dupes (sort file-editor-save-dirs #'string<)))
  (setq file-editor-save-files
        (remove-dupes (sort file-editor-save-files #'string<))))

ido has code that stores history between sessions. I’ve stolen most of it to save the file editor history. (ido-pp ...) pretty prints the variable contents into the buffer, e.g. something like this.

;; ----- file-editor-save-dirs -----

( "dir1" "dir2" "dir3" )
(require 'ido)

(defun file-editor-save-history ()
  "Save file-editor history between sessions."
  (let ((buf (get-buffer-create " *file-editor data*"))
        (version-control 'never))
        (with-current-buffer buf
          (ido-pp 'file-editor-save-dirs)
          (ido-pp 'file-editor-save-files)
          (write-file file-editor-history-file nil))
      (kill-buffer buf))))

When it comes time to load the history back, (read (current-buffer)) loads it back into the variables. You can see the use of unwind-protect and condtion-case in the code below as I talked about in my emacs lisp error handling post.

(defun file-editor-load-history ()
  (let ((file (expand-file-name file-editor-history-file)) buf)
    (when (file-readable-p file)
      (let ((buf (get-buffer-create " *file-editor data*")))
            (with-current-buffer buf
              (insert-file-contents file)
              (condition-case nil
                  (setq file-editor-save-dirs (read (current-buffer))
                        file-editor-save-files (read (current-buffer)))
                (error nil)))
          (kill-buffer buf))))))

The obvious time to save the history is when we exit emacs.

(defun file-editor-kill-emacs-hook ()

(add-hook 'kill-emacs-hook 'file-editor-kill-emacs-hook)

Modifications To The Original Code

The way we choose which files and directories will be remembered is each time a file is opened, the parent directory and the file including full path are added to the appropriate variable.

(defun file-editor-open-file-editor-file (button)
  (let ((parent (button-get button 'parent))
        (file (button-get button 'file))
        (file-complete (concat parent "/" file)))
    (push parent file-editor-save-dirs)        
    (push file-complete file-editor-save-files)
    (find-file file-complete)
    (longlines-mode 1)))

We need to extend the file-editor-default-dirs function to display the previously stored directories and files.

(defun file-editor-default-dirs ()
  (let ((buffer (get-buffer-create "*file-editor-dir-list*")))
    (with-current-buffer buffer
      (let ((inhibit-read-only t))

        (insert "*** Default File List ***\n\n")

        (dolist (dir file-editor-default-dirs)
          (file-editor-insert-opendir-button "" dir))

        (when file-editor-save-dirs
          (insert "\n")
          (dolist (dir file-editor-save-dirs)
            (file-editor-insert-opendir-button "" dir)))

        (when file-editor-save-files
          (insert "\n")
          (dolist (file file-editor-save-files)
            (insert-text-button file 'parent "" 'file file
                                :type 'open-file-editor)))))))

And for some future functionality I am thinking about we also store the current directory that is being visited.

(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))
        (setq file-editor-current-dir parent)
        ;; ...

Read Full Post »

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 »