Echo AreaJust a blog, ok2021-09-11T09:03:08Ztekutihttp://blog.ryuslash.org/feed/atomTom Willemsenhttp://blog.ryuslash.org/Combining Shell and Lisp in Eshellhttp://blog.ryuslash.org/2021/09/11/combining-shell-and-lisp-in-eshell2021-09-11T09:03:08Z2021-09-11T09:03:08Z

The code in this post is entirely useless since Perforce already provides this feature out of the box, I just didn't know about it at the time. Still, I wanted to post something and this seemed as fun as anything else.

I have been working on vc-p4 off-and-on for a while to make working with Perforce more enjoyable in Emacs. I have some plans for that package. One of the bigger things that I have done so far was add the option to specify the client in the .dir-locals.el.

So in my .dir-locals.el I would have something along the lines of the following:

((nil . (vc-p4-client . "SOME-CLIENT-NAME")))

This would let Emacs switch automatically between the different clients.

I wanted to use the p4 command-line rather than P4V more, but a thing that bugged me was that this didn't allow me to automatically pick the client, and I didn't like having to type in p4 -c SOME-CLIENT-NAME ... all the time. I felt like with Emacs, Eshell, and the feature I added to vc-p4 surely I should be able to do something about this.

The first problem I ran into is that Eshell doesn't load the directory-local variables when I change into a directory. So I wrote something to do that:

(defun oni-eshell-set-local-variables ()
  (dolist (elt file-local-variables-alist)
    (set (make-local-variable (car elt)) (default-value (car elt))))
  (setq file-local-variables-alist nil)
  (hack-dir-local-variables-non-file-buffer))

First it goes through all of the local variables that have been set before and resets them to their default value. This is so that any variables that are set locally don't hang around when you leave the directory. It then sets the list of currently set file local variables to nil so that it doesn't consider them cached and skips over giving them new values. Finally it calls the hack-dir-local-variables-non-file-buffer function that specifically exists to set directory-local values for variables in a buffer that isn't associated with a file.

(add-hook 'eshell-directory-change-hook #'oni-eshell-set-local-variables)

The function needs to run every single time the current directory changes, which Eshell has a hook for.

This would let me use a single variable essentially for specifying the current client to use. As long as there is a value this would work:

p4 --client $vc-p4-client ...

That's definitely easier than having to remember exactly which client I was using. It can still be better. I know that in Eshell shell-like constructs can be combined with Lisp easily by using either ${} or $(). So really I can just use a single command that checks whether there is a value for the client or not and calls p4 accordingly:

p4 $(when vc-p4-client (list "--client" vc-p4-client)) ...

This is very wordy though. Since this can be called any time it's nice to just make it an alias:

alias p4 'p4 $(when vc-p4-client (list "--client" vc-p4-client)) $*'

Now I can just call p4 and it'll specify the client for me automatically.

I haven't done a lot of stuff in Eshell, so I liked being able to write a fun little alias that combined some shell command with some simple Lisp code. Of course only after this I discovered (well, someone pointed out) that Perforce has the P4CONFIG environment variable, which names a file name to look for up the directory tree from the current directory (much like the .dir-locals.el works) and read settings from there. So I set that p4 set P4CONFIG=.p4config, and then specify the client in there.

P4CONFIG=SOME_CLIENT_NAME

And now I don't have to go through any of this, I can remove the whole feature I added to vc-p4 too.

Tom Willemsenhttp://blog.ryuslash.org/C-d to close eshellhttp://blog.ryuslash.org/2013/08/17/cd-to-close-eshell2013-08-17T00:25:51Z2013-08-17T00:25:51Z

One of the "tricks" that I have learned to use when working with terminals is using C-d to close them, or when working on a TTY logout. It somehow grew to the extent that if I can't use it, I get annoyed, like with eshell.

I have customized ansi-term to immediately close its buffer after the shell quits. This makes it very easy to start an ansi-term, which I've bound to C-c t, run a quick command (perhaps make, or similar), C-d, and I'm out. I want that for my eshell too.

There are a few conditions that I want met before the buffer is killed, though.

  1. Since eshell is an Emacs mode like any other, C-d is usually used
    to forward-kill characters, I don't want to lose this.

  2. I only want it to quit when the line of input is empty.

The following piece of code make sure these conditions are met.

  1. It interactively calls delete-char, which keeps keybindings like
    C-4 C-d to delete 4 characters working.

  2. It catches the error condition which is signaled wheneverdelete-char can't do it's job (like when there's nothing left to
    delete in the buffer).

  3. It checks to make sure that the signaled error is the end-of-buffer
    error. I don't want to kill the buffer if I try to delete more
    characters than are in the buffer because I feel that could cause
    irritating surprises.

  4. It checks of the cursor is at the eshell prompt. This, combined
    with only responding to the end-of-buffer error, makes sure we're
    on an empty line and not just at the end of the input. Sometimes
    keys are pressed at the wrong time and I don't want to have to
    re-type a command just because I was being an idiot.

  5. If the right conditions aren't met, signal the error again so I can
    see what's going on.

(defun eshell-C-d ()
  "Either call `delete-char' interactively or quit."
  (interactive)
  (condition-case err
      (call-interactively #'delete-char)
    (error (if (and (eq (car err) 'end-of-buffer)
                    (looking-back eshell-prompt-regexp))
               (kill-buffer)
             (signal (car err) (cdr err))))))

I then bind this to C-d in eshell.

(add-hook 'eshell-mode-hook
          (lambda () (local-set-key (kbd "C-d") #'eshell-C-d)))