Skip to content

CsBigDataHub/popterm.el

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

44 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

popterm.el

https://github.com/CsBigDataHub/popterm.el/actions/workflows/tests.yml/badge.svg

https://melpa.org/packages/popterm-badge.svg

demo

gif

A smart, posframe-based terminal toggler for Emacs.

popterm merges the best paradigms of modern Emacs terminal management into a single package. It combines centered child-frame display, the named-instance multiplexing of toggle-term.el, and the TRAMP-aware scope-locking of vterm-toggle.

Whether you use vterm, ghostel, eat, shell, or eshell, popterm provides a context-aware pop-up terminal experience that respects your window layouts, asynchronous compositor events, and background processes.

✨ Features

  • Multiple Display Backends: Choose between a centered posframe child-frame, a bottom window split, or a fullscreen takeover.
  • Agnostic Terminal Support: Seamlessly supports vterm, ghostel, eat, shell, and eshell out of the box. No hard dependencies requiredβ€”it auto-detects what you have installed.
  • Smart Auto-CD & TRAMP Awareness: Automatically changes the terminal directory to match the buffer you toggled it from. It correctly parses TRAMP remote paths so SSH commands execute safely on the remote host without local prefix mangling.
  • Named Instances & Scope Control: Run multiple terminal instances (e.g., β€œbuild”, β€œserver”, β€œrepl”) and restrict their cycling scope to the current project, the current frame, or globally.
  • Solid State Management:
    • Safely overrides display-buffer-alist to prevent terminal initialization artifacts (like 0Γ—0 PTY grids).
    • Reliably injects keybindings through vterm’s character-mode input traps via vterm-keymap-exceptions.

πŸ“¦ Installation

popterm requires Emacs 29.1+ and the posframe package.

Melpa

(use-package popterm
  :bind (("C-`"   . popterm-toggle)
         ("C-~"   . popterm-toggle-cd)
         ([f9]    . popterm-window-toggle))
  :config
  (setq popterm-backend        'vterm     ; or 'ghostel, 'eat, 'shell, 'eshell
        popterm-display-method 'posframe  ; or 'window, 'fullscreen
        popterm-scope          'project   ; or 'frame, 'dedicated, nil
        popterm-auto-cd        t)
  (popterm-global-mode 1))

straight.el / elpaca

(use-package popterm
  :straight (popterm :type git :host github :repo "CsBigDataHub/popterm.el")
  :bind (("C-`"   . popterm-toggle)
         ("C-~"   . popterm-toggle-cd)
         ([f9]    . popterm-window-toggle))
  :config
  (setq popterm-backend        'vterm     ; or 'ghostel, 'eat, 'shell, 'eshell
        popterm-display-method 'posframe  ; or 'window, 'fullscreen
        popterm-scope          'project   ; or 'frame, 'dedicated, nil
        popterm-auto-cd        t)
  (popterm-global-mode 1))

Manual

Clone the repository and add the directory to your load-path:

(add-to-list 'load-path "~/.emacs.d/site-lisp/popterm/")
(require 'popterm)

Dependencies

PackageRequired?Purpose
posframeRequiredChild-frame rendering
vtermOptionalvterm backend
ghostelOptionalGhostel backend (requires its module)
eatOptionaleat backend
projectBuilt-inProject-scoped buffer resolution
trampBuilt-inRemote path stripping for auto-cd

πŸš€ Usage

Global Commands

CommandDefault BindingDescription
popterm-toggleC-`Toggle the default terminal popup
popterm-toggle-cdC-~Toggle and cd to the originating buffer’s directory
popterm-toggle-namedC-c t nToggle a named instance, e.g. build, server, repl
popterm-window-togglef9Force a bottom window split regardless of display method
popterm-posframe-toggleβ€”Force posframe regardless of display method
popterm-findC-c t fcompleting-read picker over all active popterm buffers
popterm-vtermβ€”Toggle a vterm popup regardless of popterm-backend
popterm-ghostelβ€”Toggle a Ghostel popup regardless of popterm-backend
popterm-eatβ€”Toggle an eat popup regardless of popterm-backend
popterm-shellβ€”Toggle a shell popup regardless of popterm-backend
popterm-eshellβ€”Toggle an eshell popup regardless of popterm-backend

Inside the Terminal (popterm-mode)

When inside any popterm buffer, a minor mode activates navigation bindings. For vterm, Popterm also installs buffer-local passthrough exceptions so those keys reach Emacs instead of the terminal process (see How the vterm Keymap Trap Is Solved):

BindingCommandDescription
C-<next>popterm-nextCycle forward through terminals in the current scope
C-<prior>popterm-prevCycle backward
C-qpopterm-returnHide and return to the originating source buffer

Named Instances

popterm supports running multiple isolated terminal instances simultaneously. Buffers are named *popterm-BACKEND[NAME]*:

;; Open or toggle a terminal named "build"
(popterm-toggle "build")

;; Bind named instances to dedicated keys
(global-set-key (kbd "C-c t b") (lambda () (interactive) (popterm-toggle "build")))
(global-set-key (kbd "C-c t s") (lambda () (interactive) (popterm-toggle "server")))
(global-set-key (kbd "C-c t r") (lambda () (interactive) (popterm-toggle "repl")))

Inside the terminal, use C-<next> and C-<prior> to cycle between all instances in your current scope.

βš™οΈ Configuration

popterm is fully customizable via M-x customize-group RET popterm RET or via setq in your init file.

Display Method

;; Centered child frame (default, requires posframe)
(setq popterm-display-method 'posframe)

;; Bottom split window
(setq popterm-display-method 'window)

;; Full-frame takeover
(setq popterm-display-method 'fullscreen)

Terminal Backend

(setq popterm-backend 'vterm)   ; 'vterm | 'ghostel | 'eat | 'shell | 'eshell

Posframe Geometry

(setq popterm-posframe-width-ratio  0.62)  ; fraction of frame width
(setq popterm-posframe-height-ratio 0.62)  ; fraction of frame height
(setq popterm-posframe-min-width    100)   ; minimum column width
(setq popterm-posframe-border-width 3)     ; border in pixels

;; Use a custom poshandler for a different position, e.g. bottom-center:
(setq popterm-posframe-poshandler #'posframe-poshandler-frame-bottom-center)

Window Split Geometry

(setq popterm-window-height-ratio 0.30)    ; fraction of frame height
(setq popterm-window-side         'below)  ; 'below | 'above | 'left | 'right

Scope Control

;; Reuse any open popterm buffer (default)
(setq popterm-scope nil)

;; Only reuse buffers associated with the current project
(setq popterm-scope 'project)

;; Only reuse buffers not visible in another frame
(setq popterm-scope 'frame)

;; Always use exactly one buffer per backend (ignores named instances)
(setq popterm-scope 'dedicated)

Auto-CD & Creation Behavior

;; Always cd to the source buffer's directory on every toggle
(setq popterm-auto-cd t)

;; Allow popterm-toggle-cd to create a new terminal when none exists in scope
(setq popterm-cd-auto-create-buffer t)

Wayland / pgtk Focus Delay

On slow Wayland compositors the async focus-transfer window may exceed the default 0.35 second guard. Increase popterm-posframe-focus-delay if the posframe occasionally re-hides itself immediately after opening:

(setq popterm-posframe-focus-delay 0.5)

πŸ— Architecture & Design Notes

How the vterm Keymap Trap Is Solved

vterm (and eat) intercept keystrokes at the process-filter level, before Emacs’s minor-mode keymap lookup ever runs. popterm solves this by registering its navigation keys from vterm-mode-hook, so each vterm buffer gets the exceptions added buffer-locally when popterm-global-mode is enabled:

(add-hook 'vterm-mode-hook #'popterm--vterm-setup)

This instructs libvterm to hand the key back to Emacs rather than forwarding it to the underlying PTY.

How the Posframe Hidehandler Works

posframe ships with an idle-timer daemon (posframe-hidehandler-daemon) that fires every 0.5 s and calls each posframe’s :hidehandler function. When the handler returns non-nil, posframe hides that child frame automatically.

popterm provides popterm--posframe-hidehandler, modelled after Centaur Emacs’s shell-pop-posframe-hidehandler, which only returns non-nil (hide) when ALL of the following conditions are met:

  1. The focus-delay inhibit guard (popterm--inhibit-hidehandler) has expired.
  2. The posframe is still live and visible.
  3. The minibuffer is NOT active β€” guards standard minibuffer completion (M-x, etc.).
  4. The selected-frame is NOT a child frame β€” guards childframe completion UIs such as vertico-posframe, corfu-posframe, or company-posframe.
  5. The selected-frame is neither the popterm frame nor its parent β€” the parent check ensures the posframe stays visible while the user is in their normal workspace frame.

The inhibit timer (popterm-posframe-focus-delay seconds, stored in popterm--focus-timer) prevents the Wayland/pgtk async focus race: on these compositors, focus events can arrive before select-frame-set-input-focus completes, causing the hidehandler to fire prematurely on rapid toggle.

When the hidehandler does decide to hide, popterm also clears its own posframe session state (focus guard, display-buffer guard, theme refresh watch, and timers) before the child frame is dismissed. This prevents stale guards from lingering after an idle-timer hide and avoids accidentally re-showing the terminal on a later theme change.

How Backend Creation Avoids Display Artifacts

vterm and eat call pop-to-buffer internally, which would normally disrupt the active window layout. popterm overrides display-buffer-alist dynamically during buffer creation to force display-buffer-no-window, keeping the current layout intact while still allowing PTY-backed terminals to query correct frame dimensions for their initial grid size:

(let ((display-buffer-alist
       '((".*" (display-buffer-no-window) (allow-no-window . t)))))
  (save-current-buffer
    ;; terminal constructor called here
    ))

This is preferable to save-window-excursion, which hides window dimensions and can produce a 0Γ—0 initial PTY grid.

ghostel uses a different public entry point: it switches to the new terminal buffer and derives its default name internally. popterm wraps that command in save-window-excursion to preserve the user’s layout, then renames the created buffer into Popterm’s canonical *popterm-ghostel* or *popterm-ghostel[NAME]* naming scheme.

How Dead Source Buffers Are Handled

When popterm-return is called, popterm distinguishes between display modes to avoid a β€œfriendly-fire” bury-buffer trap:

  • Fullscreen: The terminal is still the current-buffer when hide runs, so bury-buffer correctly targets the terminal.
  • Posframe/Window: popterm--hide has already transferred focus back to the parent frame before the return logic runs. Calling bury-buffer here would accidentally bury the user’s code file. popterm emits a message instead and leaves the workspace untouched.

πŸ” Troubleshooting

Posframe closes immediately after opening

You are likely on Wayland/pgtk. Increase the focus delay guard:

(setq popterm-posframe-focus-delay 0.5)

C-q / C-<next> not working inside vterm

Ensure vterm-keymap-exceptions contains the keys. popterm registers them automatically from vterm-mode-hook when popterm-global-mode is enabled. If you do not use popterm-global-mode, add the hook manually:

(add-hook 'vterm-mode-hook #'popterm--vterm-setup)

Terminal opens as a blank buffer (eat backend)

This occurs if eat-buffer-name is not respected by your version of eat. Check that your eat package is up to date (NonGNU ELPA or Codeberg master branch). popterm binds eat-buffer-name before calling (eat) and fetches the buffer by name afterward β€” both steps require a current eat.

Ghostel backend does not start

Ensure the ghostel package itself is installed and that its native module is available. Running M-x ghostel directly should succeed before debugging Popterm integration. If Ghostel prompts to download or compile its module, let that complete first; Popterm calls Ghostel’s public entry point and does not bypass Ghostel’s own module checks.

Auto-cd does not work on remote (TRAMP) buffers

popterm strips the TRAMP remote prefix from the path before sending the cd command, so the command is valid on the remote host. Verify with:

(popterm-cd-string (current-buffer))  ; should return "cd /remote/local/path"

If the path still contains the TRAMP prefix, your TRAMP connection may not have a localname component. File a bug with the output of the above.

Named instances are not found after a session restart

popterm does not persist buffer state across Emacs restarts. Named instance buffers must be re-created each session. Use popterm-find (M-x popterm-find) to survey all active instances at any time.

Conflict when opening multiple posframe

This behavior is related to a known upstream posframe issue: posframe#155.

This is now handled natively by popterm--posframe-hidehandler. The hidehandler combines the focus-delay timer, parent/child-frame checks, and session-state cleanup to prevent spurious hides when a completion child frame (vertico-posframe, corfu-posframe, etc.) opens while popterm is visible:

  • (active-minibuffer-window) β€” guards standard minibuffer completion.
  • (frame-parameter (selected-frame) 'parent-frame) β€” guards all child-frame UIs.
  • (memq (selected-frame) (list popterm--frame parent)) β€” keeps the posframe visible while the workspace frame or the popterm frame itself is focused.
  • Internal cleanup on daemon hide β€” removes stale guards and timers so the posframe does not resurrect unexpectedly on later theme changes.

No user-side workaround is needed. If you had previously installed an advice override for popterm--after-focus-change, you can safely remove it β€” that function no longer exists.

πŸ“Š Comparison with Similar Packages

Featurepoptermvterm-toggletoggle-term
Posframe child-frame displayβœ…βŒβŒ
Window split displayβœ…βœ…βœ…
vterm backendβœ…βœ…βœ…
Ghostel backendβœ…βŒβŒ
eat backendβœ…βŒβœ…
shell / eshell backendβœ…βŒβœ…
Named instancesβœ…βœ…βœ…
completing-read pickerβœ…βŒβœ…
Project scopeβœ…βœ…βŒ
Frame scopeβœ…βœ…βŒ
Auto-cd to source bufferβœ…βœ…βŒ
TRAMP-aware auto-cdβœ…βœ…βŒ

Known issues.

  • PGTK emacs builds have a child frame focus issue

    please see #1 and Emacs bug#64625

    I recommand using lucid, x11 or --with-x-toolkit=gtk3 emacs builds on linux

πŸ§ͺ Testing

popterm includes a comprehensive test suite using ERT (Emacs Lisp Regression Testing).

Running Tests Interactively

Load the test file and run all tests:

(load-file "popterm-tests.el")
(ert-run-tests-interactively "^popterm-")

Or use the convenience function:

M-x popterm-run-all-tests

Running Tests from Command Line

For CI/CD integration or batch testing:

# Run all tests
emacs -batch -l ert -l popterm.el -l popterm-tests.el -f ert-run-tests-batch-and-exit

# Run specific test
emacs -batch -l ert -l popterm.el -l popterm-tests.el \
  --eval "(ert-run-tests-batch-and-exit 'popterm-test-buffer-name)"

Running Specific Test Categories

;; Run only performance tests
(ert-run-tests-interactively '(tag :performance))

;; Run a specific test
M-x ert RET popterm-test-buffer-name RET

Test Coverage

The test suite covers:

  • Basic API: Buffer name generation, mode mapping, backend wrapper dispatch
  • Ghostel Integration: Buffer creation/renaming and Ghostel auto-~cd~ behavior
  • Directory & Path Handling: Local/remote paths, TRAMP awareness, cd command generation
  • Scope & Filtering: Project scope, frame scope, multi-frame visibility, buffer list filtering
  • Guard Installation: Idempotent installation/removal of display-buffer, focus, and theme guards
  • Timer Management: Timer cleanup, leak prevention
  • Closure Capture: Frame capture in async callbacks
  • Session State: Active display-method tracking, forced posframe/window sessions, daemon-hide cleanup
  • Code Quality: Byte-compilation, frame capture race condition fix, timer cleanup in hide, vterm-setup docstring
  • Integration: Display-buffer-guard predicate, posframe visibility checks, theme-refresh behavior
  • Performance: Buffer list filtering with 50+ buffers

Expected Output

All tests should pass. The exact number of tests and runtime will change as the suite evolves, so treat the batch output as informational rather than fixed.

🀝 Contributing

Bug reports, feature requests, and pull requests are welcome.

Before submitting a patch, please ensure:

  1. The file byte-compiles cleanly: M-x emacs-lisp-byte-compile
  2. checkdoc passes: M-x checkdoc
  3. All tests pass: M-x popterm-run-all-tests
  4. All public functions have docstrings that uppercase argument names (e.g., NAME, BACKEND)
  5. New defcustom entries have a complete standalone first-sentence docstring
  6. Messages begin with a capital letter: "Popterm: ..."
  7. New features include corresponding test coverage in popterm-tests.el

πŸ“œ License

This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3, or (at your option) any later version.

πŸ™ Acknowledgments

popterm stands on the shoulders of:

  • seagle0128/.emacs.d β€” posframe shell-pop pattern and child-frame geometry
  • toggle-term.el β€” multi-backend architecture and named instance model
  • vterm-toggle β€” auto-cd, TRAMP path handling, and scope-locking design
  • posframe β€” the child-frame rendering library that makes this possible

About

Smart Emacs terminal toggler with posframe/window/fullscreen display, vterm/eat/shell/eshell backends, named instances, project-scoped cycling, and TRAMP-aware cd.

Topics

Resources

License

Stars

Watchers

Forks