Skip to content

How I used gitstatus.el for vterm in the header-line #2

Description

@Martinsos

Hi, thanks for this amazing package! I had a hard time finding a way to obtain git status info in elisp, and your package was perfect once I found it, due to a simple interface it has. Also while I didn't like the idea of using external gitstatusd at first, I do like how fast it is (and popular/battle-tested).

While your package doesn't have direct support for vterm, I found your implementation of eshell support very useful, as I was able to look at it and inform my effort of implementing it for vterm.
I wanted to share what I did here with you in case you or somebody else finds it useful, but also I would appreciate any thoughts on what I could have done better :).

Image

Main challenges I had while implementing this:

  • Header line refreshes on every buffer change, so simply evaluating git status calculation logic each time via :eval (which is the typical approach) would be too expensive. I solved this by using an intermediary variable my/vterm-git-status-string which is evaluated by the header line via :eval on each refresh, but is updated less often, only on the new prompt in the terminal.
  • Me wanting to run git status calculation logic on every new prompt in the terminal became a new challenge: there is no such hook in vterm. I ended up implementing my own hook by adding my custom OSC sequence prompt to the terminal prompt (PS1 in bash) and then using vterm's vterm-eval-cmds feature of vterm to run git status logic when that sequence is read. This was fun, I didn't know about OSC before this!

The custom hook that triggers on prompt in vterm:

  (with-eval-after-load 'vterm
    (defvar my/vterm-prompt-hook nil "A hook that runs each time the prompt is printed in vterm.")

    (defun my/run-vterm-prompt-hooks ()
      "Runs my/vterm-prompt-hook hooks."
      (run-hooks 'my/vterm-prompt-hook)
    )

    (with-eval-after-load 'vterm
      ;; If OSC sequence "prompt" is printed in the terminal, `my/run-vterm-prompt-hook'
      ;; will be run.
      (add-to-list 'vterm-eval-cmds '("prompt" my/run-vterm-prompt-hooks))
    )
  )

Custom header line + git status info calculation/fetching:

  (with-eval-after-load 'vterm
    (defvar-local my/vterm-git-status-string nil
      "A pretty string that shows git status of the current working directory in vterm.")

    ;; TODO: Sometimes, vterm hides top line under the header-line. But not always. It starts in right
    ;; place, and commands like "go to first line" work correctly, but I press enter and new line in
    ;; vterm appears, whole buffer shifts for one line up and the first line becomes hidden. Figure
    ;; out how to fix this.
    (defun my/vterm-set-header-line ()
      "Display the header line that shows vterm's current dir and git status.
  It gets git status string from `my/vterm-git-status-string' variable each time it renders."
      (setq header-line-format
            '((:eval (when my/vterm-git-status-string (concat " " my/vterm-git-status-string "")))
              (:propertize
               (:eval (abbreviate-file-name default-directory))
               face font-lock-comment-face
              )
             )
      )
      ;; Setting :box of header line to have an "invisible" line (same color as background) is the trick
      ;; to add some padding to the header line.
      (face-remap-add-relative
       'header-line
       `(:box (:line-width 6 :color ,(face-attribute 'header-line :background nil t)))
      )
    )
    (add-hook 'vterm-mode-hook 'my/vterm-set-header-line)

    (with-eval-after-load 'gitstatus
      (defun my/obtain-vterm-git-status-string ()
        "Obtains the git status for the current directory of the vterm buffer.
  It builds a pretty string based showing it and stores it in `my/vterm-git-status-string' var.
  It uses external `gitstatusd' program to calculate the actual git status."
        (gitstatusd-get-status
         default-directory
         (lambda (res)
           (let ((status-string (gitstatus-build-str res)))
             (when (not (equal my/vterm-git-status-string status-string))
               (setq my/vterm-git-status-string (gitstatus-build-str res))
               (force-mode-line-update)
             )
           )
         )
        )
      )
      (add-hook 'my/vterm-prompt-hook 'my/obtain-vterm-git-status-string)
    )
  )

Here is link to this same code in my Emacs.org, for the fuller picture: https://github.com/Martinsos/dotfiles/blob/c461bdce8617405252a0bd9cf86f0ccb2411ea71/vanilla-emacs.d/Emacs.org#terminal-shell .

One thing I wonder if I did wrong / didn't pay enough attention to - I saw that in you eshell code you were saving gitstatusd request id and when creating new request you would delete the old id (and also cancel the request?) and store the new id. I believe you did it due to specific of how you were detecting to wihch buffer belongs the response, but maybe there was more to it -> making sure that there are no race conditions or something like that? I thought about it a bit but couldn't find obvious practical issue of two requests running at the same time so I went with a simpelr / more naive approach, as you will be able to see above. I wonder if I missed something though.

Thanks again, and if you have some time to comment / offer advice, it is very appreciated, if not no worries, we can close this issue and it can serve as a documentation for somebody else if they will want to use gitstatus.el in vterm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions