emacs-libvterm icon indicating copy to clipboard operation
emacs-libvterm copied to clipboard

Using a tmp buffer instead of less when piping the output of a command

Open ram535 opened this issue 4 years ago • 2 comments

I thought it would be nice to pipe the output of commands to a tmp buffer. I came up with this solution.

#.bashrc
function menos() 
  {content="$(</dev/stdin)";
  vterm_cmd menos "$content"; }
}

;; init.el
(defun my/menos (content)
  (switch-to-buffer (make-temp-name "menos-"))
  (insert content))

(push (list "menos" 'my/menos)
      vterm-eval-cmds)

Now I can do something like this ls -l | menos and a buffer will open with the output of that command. menos is less in spanish.

The problem with this solution

It does not respect the format of command output. How to make it to respect the format of the command output.

ram535 avatar May 05 '21 18:05 ram535

You can reproduce the problem with the function say. E.g., say $(ls -l). I think that we are not correctly handling the newlines when executing elisp. The string arrives at vterm--eval already without the newlines, so probably we drop them in the where we create elisp_code in vterm-module.c (but I might be wrong).

I think that this is a bug.

Sbozzolo avatar May 05 '21 18:05 Sbozzolo

@ram535 I found a solution which works well for piping commands to emacs:

The bash function uses mapcar which reads from stdin into an array, the newlines are replaced with '#nl#' before being sent to vterm_cmd.

vp() {
    # If receiving from stdin
    if [ -p /dev/stdin ]; then

        pipe_input=()

        # read all lines into an array
        mapfile -n 0 -t pipe_input || {
            echo "ERROR: mapfile failed in" "${FUNCNAME}"
            exit 1
        }

        # using the array directly fails for long outputs
        # vterm_cmd bash_pipe "${pipe_input[@]}"

        # replace newlines with '#nl#'
        nl_arr=$(printf '%b#nl#' "${pipe_input[@]}")
        vterm_cmd bash_pipe "${nl_arr}"

        # the preceding fails for files containing escape chars like ''
        # maybe use the '%q' which escapes non-printable chars instead?
        # null_arr=$(printf '%q\0' "${pipe_input[@]}")
    else
        vterm_cmd bash_pipe "$*"
    fi
}

The receiving function then replaces '#nl#' with newlines and copies the result to a temporary buffer and to the clipboard.

(defun vterm-bash-pipe(vterm-out)
  "Read the output of bash `vp` function defined in ~/.bash_vterm.
The output is appended to the *vterm-out* buffer and copied to the clipboard."
  (let ((buf (get-buffer-create "*vterm-out*"))
        ;;`vp` replaces newlines with '#nl#'
        (out (replace-regexp-in-string "#nl#" "\n" vterm-out))
        (now (format-time-string "%Y%d%m-%H%M")))
    (set-buffer buf)
    ;; add date + time header
    (goto-char (point-max))
    (insert "\n## " now "\n")
    (let ((beg (point)))
      (insert out)
      (let ((end (point)))
        (insert "################")
        ;; copy the region to clipboard
        (clipboard-kill-ring-save beg end)))))

https://gist.github.com/phammar/421982159ad1995e89d7ea645fb76927

phammar avatar Sep 04 '22 16:09 phammar

This is what I've come up with for my setup:

In ~/.bashrc:

vterm_less() {
    printf "\e]51;Eless \""
    cat | base64 -w0
    printf "\"\e\\"
}
if [[ "$INSIDE_EMACS" = 'vterm' ]]; then
    alias less=vterm_less
fi

In ~/.emacs.d/init.el:

(defun vterm-less (content)
  (let ((less-buffer (get-buffer-create (make-temp-name "vterm-less-"))))
    (with-current-buffer less-buffer
      (switch-to-buffer less-buffer)
      (special-mode)
      (insert (base64-decode-string content)))
    )
  )

(defun my-vterm-mode-hook ()
  (add-to-list 'vterm-eval-cmds (list "less" #'vterm-less))
  )
(add-hook 'vterm-mode-hook #'my-vterm-mode-hook)

It works fine for up to 100k lines. After 10k lines it takes quite a bit of time to pass the content though. I suspect this is because how the event loop is written. Nevertheless, it works and preserves all formatting.

knazarov avatar Jan 08 '23 20:01 knazarov

This works awesome for getting command output into Emacs! Thank you all, @ram535, @phammar, @knazarov! I think this would be an awesome feature to have built-in which would greatly enhance Emacs/vterm integration.

I'm using @knazarov's solution which works great so far, with some minor annoyances:

  • I get some special chars as codes in the buffer, eg. Catalan apostrophe as \342\200\231, accents such as í as \303\255 and some emojis and Nerd icons. ANSI colors are correctly ignored. No big deal, so far. I'll get to it if I have time (any advice appreciated).
  • I've found the hook code is not needed.

@Sbozzolo What would be needed to integrate this in the package in order to submit a PR? Some ideas:

  • A .bashrc command documented in the readme, such as vterm-pipe.
  • Add the Elisp function, such as vterm-pipe-read.
  • No clipboard copying (at least by default). Maybe a vterm-pipe-copy function based on the above to get the piped text straight into the kill ring.
  • Anything else?

Ideally, vterm-pipe-read would be basic plumbing, so it can be used to pipe to buffer, clipboard, etc. depending on some parameter, which would have corresponding shell commands as vterm-pipe-{buffer,copy,etc}. Not sure how to implement and expose it, thou.

Comments/feedback welcome.

pataquets avatar May 03 '23 18:05 pataquets

None of the above worked for me in all my use cases, so I went for a simpler approach: use cat as the PAGER and rely on vterm builtin navigation functions.

Main drawback is that it fills up you vterm buffer. Should be as fast as vterm is able to display lines. Didn't try it out for huge outputs, feedback is very welcome.

Just add this to .bashrc

if [[ "$INSIDE_EMACS" = 'vterm' ]]; then
    export PAGER=cat
fi

and use C-c C-p to jump to the previous prompt. Then navigate the output using copy-mode.

Jumping back and going to copy-mode can be done automatically if you add these utils: In some script ~/my-pager:

#!/usr/bin/env sh

if [[ "$INSIDE_EMACS" = 'vterm' ]]; then
    cat
    printf "\e]51;Epager"
    printf "\e\\"
else
    less
fi

in .bashrc:

export PAGER=~/my-pager

in init.el:

(defun vterm-pager ()
  (vterm-previous-prompt 1)
  (vterm-copy-mode))

(add-to-list 'vterm-eval-cmds (list "pager" #'vterm-pager))

theottm avatar May 11 '23 10:05 theottm

~@ram535~ @jixiuf @Sbozzolo Any feedback from maintainers about the possibility of accepting a PR for this feature as discussed in my prior comment?

EDIT: mentioned ram535 as maintainer originally by mistake, as he was the issue closer. Apologies for the noise.

pataquets avatar Oct 20 '23 23:10 pataquets