akermu / emacs-libvterm

Emacs libvterm integration
GNU General Public License v3.0
1.7k stars 136 forks source link

Update default-directory #55

Closed deadtrickster closed 5 years ago

deadtrickster commented 5 years ago

Is it possible to update default-directory after cd so I can easily find files? At least title updated somehow...

deadtrickster commented 5 years ago

This is how I hacked it for now:

(defun vterm--rename-buffer-as-title (title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (cd-absolute dir)
  (rename-buffer (format "term %s" title)))
(add-hook 'vterm-set-title-functions 'vterm--rename-buffer-as-title)

title is <user>@<host>:path

deb0ch commented 5 years ago

I saw a PR and a few commits about that recently, they called it "directory tracking". I think you can find it in the recently closed PRs. If I recall correctly you have to setup some stuff in your shell's config files as well for it to work.

jhacksworth commented 5 years ago

@deadtrickster Thanks for this! Now C-x C-f works the way it should. 😃

For me, removing the buffer renaming and just updating the directory allows for multiple vterm buffers to have the same current directory.

(defun vterm--cd-to-directory-in-title (title)
  "Change into directory extracted from path info in TITLE.

The format of TITLE is expected to be '<user>@<host>:path'

TITLE is based on the xterm style window title, set via the shell. See sections
3 and 4 on this page for how to set the TITLE via the shell:
http://www.faqs.org/docs/Linux-mini/Xterm-Title.html"
  (let ((dir (concat (nth 1 (split-string title ":")) "/")))
    (cd dir)))
(add-hook 'vterm-set-title-functions #'vterm--cd-to-directory-in-title)
QiangF commented 5 years ago

I think leaving cwd in the buffer name helps a lot in finding the right vterm.

jixiuf commented 5 years ago

For me, removing the buffer renaming and just updating the directory allows for multiple vterm buffers to have the same current directory.

(rename-buffer NEWNAME &optional UNIQUE)

:around advice: ‘uniquify--rename-buffer-advice’

Change current buffer’s name to NEWNAME (a string). If second arg UNIQUE is nil or omitted, it is an error if a buffer named NEWNAME already exists. If UNIQUE is non-nil, come up with a new name using ‘generate-new-buffer-name’. Interactively, you can set UNIQUE with a prefix argument. We return the name we actually gave the buffer. This does not change the name of the visited file (if any).

(defun vterm-buffer-name(prefix cmd default-directory) (let* ((cmd (car (split-string cmd "[ |\t]" t " "))) (pwd (abbreviate-file-name default-directory)) (dir-tokens (split-string pwd "[/|\]" t " "))) (when (> (length dir-tokens) 2) (setq pwd (mapconcat 'identity (last dir-tokens 2) "/"))) (format "%s%s(%s)" prefix (or cmd "") pwd)))

(rename-buffer (vterm-buffer-name "vterm " "pwd" "~/a/b/c/d/e") t)


result:

"vterm pwd(d/e)"

jhacksworth commented 5 years ago

Thanks @jixiuf. Changing

  (rename-buffer (format "term %s" title)))

to

  (rename-buffer (format "term %s" title) t))

solves the multiple vterms with same directory problem. 👍

deadtrickster commented 5 years ago

I also have this snippet:

(make-local-variable 'vterm-is-for-project)
(setq-default vterm-is-for-project nil)

(defun vterm--rename-buffer-as-title (title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (cd-absolute dir)
    (if (and vterm-is-for-project (not (projectile-project-p)))
        ;; we left project directory
        (setq-local vterm-is-for-project nil))
    (unless vterm-is-for-project
      (rename-buffer (vterm--buffer-title) t))))

(add-hook 'vterm-set-title-functions 'vterm--rename-buffer-as-title)

(defun vterm--buffer-title ()
  (format "term %s@%s:%s" user-login-name (system-name) default-directory))

(defun vterm--project-buffer-title ()
  (format "term %s" (projectile-project-name)))

(defun vt ()
  "Create a new vterm."
  (interactive)
  (let* ((buffer-title (vterm--buffer-title))
         (existing-buffer (get-buffer buffer-title)))
    (if existing-buffer
        (switch-to-buffer existing-buffer)
      (let ((buffer (generate-new-buffer buffer-title)))
        (with-current-buffer buffer
          (vterm-mode))
        (switch-to-buffer buffer)))))

(defun vtp ()
  "Create a new vterm."
  (interactive)
  (let* ((buffer-title (vterm--project-buffer-title))
         (existing-buffer (get-buffer buffer-title)))
    (if existing-buffer
        (switch-to-buffer existing-buffer)
      (let ((buffer (generate-new-buffer buffer-title)))
        (with-current-buffer buffer
          (cd (projectile-project-root))
          (vterm-mode)
          (setq-local vterm-is-for-project t))
        (switch-to-buffer buffer)))))

Ugly and buggy as all my elisp (sigh) but vtp (re)opens the terminal for the current project, which I've found very handy.

Sbozzolo commented 5 years ago

Since this is a very important feature (at least, in my humble opinion), wouldn't be useful to add one of the suggested solutions to the readme?

deadtrickster commented 5 years ago

Updated code to fix https://github.com/akermu/emacs-libvterm/issues/60

jhacksworth commented 5 years ago

Thanks for the update, @deadtrickster, works for me. 👍 I suggest calling rename-buffer with t in addition to the new buffer name, to avoid a naming error when you have two vterms with the same directory. Also, there should be an extra paren at the end of the function definition:

(defun vterm--rename-buffer-as-title (title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (cd-absolute dir)
  (rename-buffer (format "term %s" title) t)))
(add-hook 'vterm-set-title-functions #'vterm--rename-buffer-as-title)
deadtrickster commented 5 years ago

@jhacksworth yeah I actually added t to my config :-)

deadtrickster commented 5 years ago

Updated my vterm--rename-buffer-as-title so it can escape project directories properly - basically falling back to regular terminals. Feels good.

Bee-Mar commented 5 years ago

I've tried every single one of your approaches and I still cannot get my current directory to update when using C-x C-f. Even using a completely clean emacs init. Is there something I'm missing that was left out in the conversation? The entire init file I'm testing it with is below. The use-package portion couple other irrelevant items are there simply because I always keep a very basic init file as a backup, and I just dropped the vterm stuff in there for testing.

(require 'package)

(setq package-enable-at-startup nil)
(package-initialize)

(custom-set-variables
 ;; custom-set-variables was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 '(package-archives
   (quote
    (("Marmalade" . "https://marmalade-repo.org/packages/")
     ("GNU" . "http://elpa.gnu.org/packages/")
     ("MELPA" . "http://melpa.milkbox.net/packages/")
     ("MELPA-Stable" . "https://stable.melpa.org/packages/")))))

(unless (package-installed-p 'use-package)
  (package-refresh-contents)
  (package-install 'use-package))

(setq tool-bar-mode nil)

(menu-bar-mode -1)
(scroll-bar-mode -1)
(tool-bar-mode -1)
(blink-cursor-mode -1)
(tooltip-mode 1)

(require 'use-package)
(setq use-package-always-ensure t)

;;(org-babel-load-file (expand-file-name "~/.emacs.d/tidy-init.org"))

(set-background-color "black")

(add-to-list 'load-path "~/.emacs.d/external-pkgs/emacs-libvterm")

(let (vterm-install)
  (require 'vterm))

(add-hook 'vterm-mode-hook (lambda ()
                 (setq-local global-hl-line-mode nil)
                 (setq-local truncate-lines t)))

(defun vterm--rename-buffer-as-title (title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (cd-absolute dir)
  (rename-buffer (format "term %s" title) t)))
(add-hook 'vterm-set-title-functions #'vterm--rename-buffer-as-title)

(custom-set-faces
 ;; custom-set-faces was added by Custom.
 ;; If you edit it by hand, you could mess it up, so be careful.
 ;; Your init file should contain only one such instance.
 ;; If there is more than one, they won't work right.
 )
jhacksworth commented 5 years ago

@Bee-Mar Just to be clear (because I didn't realize this at first) there are two sides to this hack: Emacs and the shell. You need to configure your shell to set the xterm window title with information that Emacs (via this hack) uses to figure out what the current directory is.

How you set the xterm title depends on your shell. For example, in Bash, you might set your PS1 prompt to

PS1='\[\033]0;\u@\h:\w\007\]\$ '

This would send the non-printing escape sequences necessary to set the xterm title, in the format that this hack expects: <user>@<host>:path.

For Bash, the non-printing escape sequence is \[\033]0;\u@\h:\w\007\].

For the specifics on how to set the xterm title, see:

Bee-Mar commented 5 years ago

@jhacksworth aaaaaaaahhhhhhhh this makes a whole lot more sense. I should have caught what the function was reading to parse the string, and produce the correct path. Well, this clears things up, and I was able to get it to work. At least this explanation is here now for others who may have had a similar question. Thanks!

Also, I just now realized, I had removed that non-printing escape sequence when I first started using multi-term because multi-term was printing that information to the screen, which was annoying. So, that also added another layer to my confusion lol.

Bee-Mar commented 5 years ago

Interestingly, I found an edge case for this function. When I ssh'd into one of my servers, I found that vterm froze up, and then I quickly realized it's because emacs was attempting to change into a directory that didn't exist on my local machine. I added in small check to ensure the host names match before this happens.

(setq user-host-name
      (concat "@"
                 (substring
                  (shell-command-to-string "hostname")
                  0 -1)
                 ":"))

;; user-host-name = "@[hostname]:"

  (defun vterm--rename-buffer-as-title (title)
    (if (string-match-p (regexp-quote user-host-name) title)
        (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
          (cd-absolute dir)
          (rename-buffer (format "term %s" title) t))))
jhacksworth commented 5 years ago

@Bee-Mar Thanks for reporting this.

I think a check to make sure the directory exists would make sense, even for people who aren't logging in to remote hosts. For example, with file-directory-p:

(defun vterm--rename-buffer-as-title (title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (if (file-directory-p dir)
        (cd-absolute dir))
    (rename-buffer (format "term %s" title) t)))
Bee-Mar commented 5 years ago

@jhacksworth that's definitely a cleaner way of handling it

jixiuf commented 5 years ago

share my snippet even in a ssh session, default-directory is still updated when you cd to some directory , so you can call dired-jump or find-file directly.

the xterm title need in the format:user@host:path or user@host@lastcmd:path

(defvar vterm-user "")
(make-variable-buffer-local 'vterm-user)
(defvar vterm-host "")
(make-variable-buffer-local 'vterm-host)
(defvar vterm-pwd "")
(make-variable-buffer-local 'vterm-pwd)
(defvar vterm-cmd "")
(make-variable-buffer-local 'vterm-cmd)
;; for Bash: 
;;PS1='\[\033]0;\u@\H:\w\007\]\$ '
;; for zsh
;; # for emacs vterm.el
;; HOSTNAME=$(uname -n)
;; USER=$(whoami)
;; case $TERM in
;;     (*xterm*|*rxvt*|(dt|k)term*))
;;         # http://zsh.sourceforge.net/Doc/Release/Functions.html
;;         # 刚打开shell时,也执行一次更新title
;;         lastcmd=""
;;         print -Pn "\e]2;${USER}@${HOSTNAME}@${lastcmd}:%~\a" #set title user@host@cmd:path
;;         preexec () {
;;             cmd="$1"
;;             tokens=(${(s/ /)cmd}) # split by space
;;             lastcmd=$tokens[1]
;;             # # 标题栏、任务栏样式
;;             # 在执行命令前执行,所以此时打印的pwd可能不准,故还需要在chpwd里,刚更新一次
;;             print -Pn "\e]2;${USER}@${HOSTNAME}@${lastcmd}:%~\a" #set title user@host@cmd:path
;;         }
;;         chpwd() {
;;             # ESC]0;stringBEL — Set icon name and window title to string
;;             # ESC]1;stringBEL — Set icon name to string
;;             # ESC]2;stringBEL — Set window title to string
;;             print -Pn "\e]2;${USER}@${HOSTNAME}@${lastcmd}:%~\a" #set title user@host@cmd:path  chpwd里取不到当前cmd
;;         }

;; esac

(defun vterm-set-title-hook (title) ;title = user@host@lastcmd:path  or user@host:path
  (let* ((tokens (split-string title ":" ))
         dir)
    (when (equal 2 (length tokens))
      (setq vterm-pwd (string-trim-left (nth 1 tokens)))
      (setq tokens (split-string (nth 0 tokens) "@" ))
      (when (>  (length tokens) 1)
        (setq vterm-user (nth 0 tokens))
        (setq vterm-host (nth 1 tokens))
        (when (and (nth 2 tokens)
                   (not (string-empty-p (or (nth 2 tokens) ""))))
          (setq vterm-cmd (nth 2 tokens))))
      (setq dir
            (file-name-as-directory
             (if (and (string= vterm-host (system-name))
                      (string= vterm-user (user-real-login-name)))
                 (expand-file-name vterm-pwd)
               (concat "/-:" vterm-user "@" vterm-host ":"
                       vterm-pwd))))
      (when (ignore-errors (file-directory-p dir))
        (cd-absolute dir))
      ;; (message "pwd=%s,user=%s,host=%s,cmd=%s d=%s"
      ;;          vterm-pwd vterm-user vterm-host vterm-cmd dir)
      (rename-buffer (format "term %s %s@%s:%s" vterm-cmd vterm-user vterm-host vterm-pwd ) t)
      )))

(add-hook 'vterm-set-title-functions 'vterm-set-title-hook)
Bee-Mar commented 5 years ago

@jixiuf does this code only work for zsh? Also, that functionality is awesome.

jixiuf commented 5 years ago

@Bee-Mar not just for zsh you just need set the xterm title, in the format : user@host:path or user@host@lastcmd:path

in Bash, you might set your PS1 prompt to

PS1='\[\033]0;\u@\H:\w\007\]\$ '

https://github.com/akermu/emacs-libvterm/issues/55#issuecomment-471334398 mentioned

Bee-Mar commented 5 years ago

@Bee-Mar not just for zsh you just need set the xterm title, in the format : user@host:path or user@host@lastcmd:path

in Bash, you might set your PS1 prompt to

PS1='\[\033]0;\u@\h:\w\007\]\$ '

#55 (comment) mentioned

My PS1 has been set correctly, but there's some other issue. When I one of my servers, the title doesn't update at all, and when I ssh into another, the tramp connection fails. I have everything setup using ssh keys, and when I remove the code you suggested everything works fine again.

jixiuf commented 5 years ago

you can add some debug code and see what's going on like

      (message "title=%s\n pwd=%s\n user=%s\nhost=%s\ncmd=%s\n dir=%s\n"
               title vterm-pwd vterm-user vterm-host vterm-cmd dir)

for me it looks like this:

title=root@BJ-DEV-GO: ~
 pwd=~
 user=root
host=BJ-DEV-GO
cmd=cd
 dir=/-:root@BJ-DEV-GO:~/

then you can try to open /-:root@BJ-DEV-GO:~/ with find-file and make sure you can ping the host maybe you need add a line in your /etc/hosts

and I changed

      (when (file-directory-p dir)

to

      (when (ignore-errors (file-directory-p dir))

in case the remote host is not connectable

jhacksworth commented 5 years ago

@jixiuf Thank you for sharing your snippet! This is awesome :)

@Bee-Mar I'm not sure what's causing your issues, but I can share some settings that helped me.

First, in my Bash shell config, I had to change the non-printing escape sequence from

 \[\033]0;\u@\h:\w\007\]

to

\[\033]0;\u@\H:\w\007\]

Note the change to uppercase \H. From the PROMPTING section of the Bash manpage:

      \h     the hostname up to the first `.'
      \H     the hostname

This was due to the hostname part in the xterm title set by Bash not matching with the Emacs (system-name).

Next, I had some issues with Tramp that were fixed by the following settings:

(setq tramp-default-method "ssh")
(setq tramp-shell-prompt-pattern "\\(?:^\\|\r\\)[^]#$%>\n]*#?[]#$%>].* *\\(^[\\[[0-9;]*[a-zA-Z] *\\)*")

I had some non-standard prompts that were fixed by editing tramp-shell-prompt-pattern.

I hope this helps.

Bee-Mar commented 5 years ago

@jhacksworth Thanks! The tramp-shell-prompt-pattern did the trick. Did you come up with that regex? Cuz that's definitely a non-obvious pattern lol

jhacksworth commented 5 years ago

@Bee-Mar Glad to hear it worked! I definitely did not come up with the regex :)

I got it from the EmacsWiki -Tramp Mode page, in the section titled Tramp hangs # 6: Not recognising the remote shell prompt

Bee-Mar commented 5 years ago

@jhacksworth aahhh, well, they certainly had a good bit of information lol

sienic commented 1 year ago

Since vterm-set-title-functions is obsolete:

A. If you want to update default-directory to be the current vterm dir

(defun sn/vterm--set-title-pre (title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (when (file-directory-p dir)
        (cd-absolute dir))))

(advice-add 'vterm--set-title :before #'sn/vterm--set-title-pre)

;; or for Doom users

(defadvice! sn/vterm--set-title-pre (title)
  :before #'vterm--set-title
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (when (file-directory-p dir)
        (cd-absolute dir))))

B. If you additionaly want to rename the buffer

Set vterm-buffer-name-string with one format control string (as required). The replaced value is the returned title from your terminal session.

;; (setq vterm-buffer-name "*vterm*") ;; you can use this custom property for convenience
(setq vterm-buffer-name-string "%s")

Once that's configured then

(defun sn/vterm--set-title-pre (fn title)
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (when (file-directory-p dir)
        (cd-absolute dir))
    (message (format "%s %s" vterm-buffer-name title))
    (funcall fn (format "%s %s" vterm-buffer-name title))))
(advice-add 'vterm--set-title :around #'sn/vterm--set-title-pre)

;; or for Doom users

(defadvice! sn/vterm--set-title-pre (fn title)
  :around #'vterm--set-title
  (let ((dir (string-trim-left (concat (nth 1 (split-string title ":")) "/"))))
    (when (file-directory-p dir)
        (cd-absolute dir))
    (funcall fn (format "%s %s" vterm-buffer-name title))))

EDIT: Just realised there's this: https://github.com/akermu/emacs-libvterm#directory-tracking-and-prompt-tracking 🤦 … anyway, this might still be an alternative