Skip to content

agzam/mxp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

38 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

mxp AKA “Emacs Piper”

https://github.com/agzam/mxp/actions/workflows/test.yml/badge.svg

Pipe content between your terminal and Emacs buffers - seamlessly bridge your command-line workflows with your editor.

Demo on YouTube, ~12min

What?

mxp is a shell script that acts as a bridge between Unix pipes and Emacs buffers. It supports:

  • Write mode: Pipe stdin to Emacs buffers (creates new or appends to existing)
  • Read mode: Output buffer content to stdout for piping
  • Streaming: Real-time content updates with chunked processing
  • Buffer matching: Use exact names or regex patterns to find buffers
  • Auto-generation: Creates *Piper 1*, *Piper 2*, etc. when no name specified
  • Conflict handling: Avoids overwriting buffers unless forced
  • Persistent socket: Auto-boots a TCP eval server inside Emacs for fast, ordered communication (falls back to emacsclient transparently)

The script works with both bash and zsh, handles special characters properly, and includes comprehensive error handling.

Do not confuse it with similarly named emacs-piper

Howard Abram’s emacs-piper project although has similarities, it works differently and has different goals.

Why?

  • Pipe command output directly into Emacs buffers for comfortable viewing
  • Stream logs in real-time to watch them in your editor
  • Extract buffer content from Emacs and pipe it to terminal commands

Did you know about piping in Eshell?

Emacs has piping in/out buffers in Eshell! You can pipe content into a buffer like this:

;; In eshell, you can pipe command output to a buffer 
;; You can press <C-c M-b> to pick a buffer instead of typing it

cat file.txt > #<buffer some-buffer> 

Unfortunately, Eshell doesn’t support input redirection, so reading from a buffer ain’t so straightforward, yet still possible. You just need to create a custom eshell command to read from a buffer, e.g.:

;; note the eshell/ prefix
(defun eshell/b (buf-or-regexp)
  "Output buffer content of buffer matching BUF-OR-REGEXP."
  (let ((buf (if (bufferp buf-or-regexp)
                 buf-or-regexp
               (cl-loop for b in (buffer-list)
                        thereis (and (string-match-p
                                      buf-or-regexp (buffer-name b))
                                     b)))))
    (when buf
      (with-current-buffer buf
        (buffer-substring-no-properties (point-min) (point-max))))))

And then we can use it in Eshell:

;; Press <C-c M-b> to pick a buffer instead of typing it

b #<buffer README.org> | rg "error"

Incredibly powerful, but this only works inside Eshell - you cannot use it in an external terminal. mxp brings this same workflow to your regular shell, letting you pipe between any terminal and Emacs.

How?

Make sure you have:

  • Emacs with emacsclient (Emacs daemon must be running)
  • bash >= 4.0 or zsh
  • Standard Unix utilities (base64, grep, sed)

Install

Download and put it somewhere in the $PATH

curl -fsSL https://raw.githubusercontent.com/agzam/mxp/refs/heads/main/mxp -o \
    ~/.local/bin/mxp \
    && chmod +x ~/.local/bin/mxp 

Make sure Emacs daemon is running and emacsclient works!

Socket transport

On first use, mxp boots a lightweight TCP eval server inside your running Emacs and connects to it directly via bash’s /dev/tcp. All subsequent calls reuse this persistent connection, which means:

  • No process spawning per eval (previously each chunk launched a new emacsclient process)
  • Guaranteed ordering for streamed content (no more background job races)
  • No “Connection refused” errors under heavy streaming load

The server starts automatically - no manual M-x step required. If the socket is unavailable (e.g., bash built without /dev/tcp support), mxp falls back to emacsclient transparently.

VariableDefaultDescription
MXP_PORT17394TCP port for the eval server (localhost only)
MXP_NO_SOCKET(unset)Set to 1 to force emacsclient-only mode

To stop the server manually:

(when (and (boundp 'mxp-server-process) (process-live-p mxp-server-process))
  (delete-process mxp-server-process)
  (setq mxp-server-process nil))

Use

Open files and directories in Emacs

# Open a file
mxp my-file.txt
mxp README.org

# Open current directory in dired
mxp .

# Open any directory
mxp ~/Projects
mxp /path/to/directory

# Works with relative paths
mxp ../other-project/file.txt

Pipe command output into Emacs

# Pipe to a named buffer
cat file.txt | mxp "my-buffer"

# Pipe to auto-generated buffer (*Piper 1*, *Piper 2*, etc.)
tail -f /var/log/app.log | mxp

# Append to existing buffer
echo "more content" | mxp --append "my-buffer"
echo "more content" | mxp -a "my-buffer"

# Prepend to existing buffer (insert at the top)
echo "header info" | mxp --prepend "my-buffer"
echo "header info" | mxp -p "my-buffer"

# Match buffer by regex
echo "data" | mxp "mybuf.*"

# Force overwrite existing buffer
cat new.txt | mxp --force "my-buffer"
cat new.txt | mxp -F "my-buffer"

Extract buffer content and pipe to commands

# Output buffer to stdout
mxp --from "my-buffer"
mxp -f "my-buffer"

# Pipe buffer to commands
mxp --from "*Messages*" | grep error
mxp -f ".*scratch.*" | wc -l

# Use in command chains
mxp -f "my-buffer" | sort | uniq | less

Process substitution

Works naturally with process substitution for commands expecting files:

# Compare two buffers
diff <(mxp -f "version-1") <(mxp -f "version-2")

# Use buffer as input file
jq . <(mxp -f "*json-data*")

Emacs Hooks

There are hooks that you can customize:

RunsArgsNotes
mxp-buffer-hook
when the buffer appearsBUFFER-NAMEuseful for setting major mode, etc.
mxp-buffer-update-hook
whenever there’s more dataBUFFER-NAME
BEG-POS,END-POS
where buffer gets updated
mxp-buffer-complete-hook
at the completionBUFFER-NAMEmay never run for continuous streams

Hook examples:

(defun on-mxp-buffer-h (buffer-name)
  (with-current-buffer buffer-name
    (when (string-match ".*\\.json.*" buffer-name)
      (json-mode))))
(add-hook 'mxp-buffer-hook #'on-mxp-buffer-h)

;; This is how you can re-apply colors. I don't want to make escape
;; color code processing built into the script itself. It's better to
;; keep that customizable.
(defun on-mxp-buffer-update-h (buffer-name beg end)
  (with-current-buffer buffer-name
    (ansi-color-apply-on-region beg end)))
(add-hook 'mxp-buffer-update-hook #'on-mxp-buffer-update-h)

(defun on-mxp-buffer-complete-h (buffer-name)
  (with-current-buffer buffer-name
    ;; delete all empty lines
    (flush-lines "^$" (point-min) (point-max))))
(add-hook 'mxp-buffer-complete-hook #'on-mxp-buffer-complete-h)

Usage examples

# Quick file/directory access
mxp config.json          
mxp .                    
mxp ~/Documents          
mxp $HOME          

# Watch build logs in Emacs
npm run build | mxp "build-logs"

# Send curl output to Emacs for inspection
curl -s "https://api.thedogapi.com/v1/breeds" | jq | mxp "breeds"
# and the the opposite direction:
mxp "breeds" | jq '.[].name' | sort | mxp "dog names"

# Extract TODO items from buffer
mxp -f "*scratch*" | grep TODO > todos.txt

# Add timestamps to the top of a log buffer
date | mxp --prepend "logs"
tail -f app.log | mxp --append "logs"

# Stitch multiple buffers together
cat <(mxp -f "header") <(mxp -f "body") | mail -s "Report" user@example.com

# Edit a file, then pipe its buffer content through a command
mxp config.yaml                           # Opens in Emacs
mxp -f config.yaml | yq '.version' -      # Read it back

# Stream some data with a passtrhough (shows results in both the buffer and terminal)
ping google.com | tee >(mxp)
Copyright © 2025 Ag Ibragimov

About

Shell script for piping things in and out of Emacs buffers

Topics

Resources

License

Stars

Watchers

Forks

Contributors